浏览代码

revert: "build: Migrate to Vite 🚀" (#6814)

Revert "build: Migrate to Vite 🚀 (#6713)"

This reverts commit e93bbc577613a6de6bb43c40c3b57286b4994ca5.
Aakansha Doshi 2 年之前
父节点
当前提交
8104068bd5
共有 100 个文件被更改,包括 1657 次插入1643 次删除
  1. 11 11
      .env.development
  2. 8 8
      .env.production
  3. 2 2
      .github/workflows/autorelease-excalidraw.yml
  4. 2 2
      .github/workflows/autorelease-preview.yml
  5. 2 2
      .github/workflows/lint.yml
  6. 2 2
      .github/workflows/locales-coverage.yml
  7. 2 2
      .github/workflows/sentry-production.yml
  8. 2 2
      .github/workflows/test.yml
  9. 0 2
      .gitignore
  10. 41 24
      package.json
  11. 15 9
      public/index.html
  12. 0 0
      public/workbox/workbox-background-sync.prod.js
  13. 2 0
      public/workbox/workbox-broadcast-update.prod.js
  14. 2 0
      public/workbox/workbox-cacheable-response.prod.js
  15. 0 0
      public/workbox/workbox-core.prod.js
  16. 0 0
      public/workbox/workbox-expiration.prod.js
  17. 2 0
      public/workbox/workbox-navigation-preload.prod.js
  18. 2 0
      public/workbox/workbox-offline-ga.prod.js
  19. 0 0
      public/workbox/workbox-precaching.prod.js
  20. 2 0
      public/workbox/workbox-range-requests.prod.js
  21. 0 0
      public/workbox/workbox-routing.prod.js
  22. 0 0
      public/workbox/workbox-strategies.prod.js
  23. 2 0
      public/workbox/workbox-streams.prod.js
  24. 2 0
      public/workbox/workbox-sw.js
  25. 0 0
      public/workbox/workbox-window.prod.es5.mjs
  26. 0 0
      public/workbox/workbox-window.prod.mjs
  27. 0 0
      public/workbox/workbox-window.prod.umd.js
  28. 1 1
      src/analytics.ts
  29. 3 2
      src/charts.ts
  30. 1 2
      src/components/App.test.tsx
  31. 9 4
      src/components/App.tsx
  32. 1 1
      src/components/LibraryMenuBrowseButton.tsx
  33. 1 1
      src/components/PublishLibrary.tsx
  34. 1 2
      src/components/Sidebar/Sidebar.test.tsx
  35. 1 1
      src/components/Sidebar/Sidebar.tsx
  36. 2 2
      src/components/__snapshots__/App.test.tsx.snap
  37. 1 1
      src/element/newElement.ts
  38. 1 17
      src/element/sizeHelpers.test.ts
  39. 12 12
      src/element/textWysiwyg.test.tsx
  40. 8 2
      src/excalidraw-app/collab/Collab.tsx
  41. 1 3
      src/excalidraw-app/components/AppWelcomeScreen.tsx
  42. 1 3
      src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx
  43. 2 4
      src/excalidraw-app/data/firebase.ts
  44. 5 5
      src/excalidraw-app/data/index.ts
  45. 31 0
      src/excalidraw-app/pwa.ts
  46. 2 2
      src/excalidraw-app/sentry.ts
  47. 10 0
      src/global.d.ts
  48. 3 2
      src/i18n.ts
  49. 2 1
      src/index.tsx
  50. 3 3
      src/packages/excalidraw/env.js
  51. 0 1
      src/packages/excalidraw/package.json
  52. 1 1
      src/packages/excalidraw/publicPath.js
  53. 0 3
      src/packages/excalidraw/webpack.dev.config.js
  54. 0 3
      src/packages/excalidraw/webpack.prod.config.js
  55. 0 5
      src/packages/excalidraw/yarn.lock
  56. 1 1
      src/renderer/renderElement.ts
  57. 3 4
      src/scene/export.ts
  58. 147 0
      src/service-worker.ts
  59. 162 0
      src/serviceWorkerRegistration.ts
  60. 8 5
      src/setupTests.ts
  61. 6 5
      src/tests/MobileMenu.test.tsx
  62. 2 2
      src/tests/__snapshots__/MobileMenu.test.tsx.snap
  63. 6 6
      src/tests/__snapshots__/charts.test.tsx.snap
  64. 183 183
      src/tests/__snapshots__/contextmenu.test.tsx.snap
  65. 31 31
      src/tests/__snapshots__/dragCreate.test.tsx.snap
  66. 2 2
      src/tests/__snapshots__/export.test.tsx.snap
  67. 3 3
      src/tests/__snapshots__/linearElementEditor.test.tsx.snap
  68. 34 34
      src/tests/__snapshots__/move.test.tsx.snap
  69. 19 19
      src/tests/__snapshots__/multiPointCreate.test.tsx.snap
  70. 182 182
      src/tests/__snapshots__/regressionTests.test.tsx.snap
  71. 27 27
      src/tests/__snapshots__/selection.test.tsx.snap
  72. 0 1
      src/tests/appState.test.tsx
  73. 4 5
      src/tests/clipboard.test.tsx
  74. 17 23
      src/tests/collab.test.tsx
  75. 1 2
      src/tests/contextmenu.test.tsx
  76. 57 57
      src/tests/data/__snapshots__/restore.test.ts.snap
  77. 5 6
      src/tests/data/restore.test.ts
  78. 1 2
      src/tests/dragCreate.test.tsx
  79. 2 3
      src/tests/fitToContent.test.tsx
  80. 14 13
      src/tests/flip.test.tsx
  81. 5 10
      src/tests/library.test.tsx
  82. 150 151
      src/tests/linearElementEditor.test.tsx
  83. 1 2
      src/tests/move.test.tsx
  84. 1 2
      src/tests/multiPointCreate.test.tsx
  85. 3 3
      src/tests/packages/__snapshots__/excalidraw.test.tsx.snap
  86. 11 11
      src/tests/packages/__snapshots__/utils.test.ts.snap
  87. 4 6
      src/tests/packages/excalidraw.test.tsx
  88. 17 13
      src/tests/packages/utils.test.ts
  89. 1 3
      src/tests/regressionTests.test.tsx
  90. 1 2
      src/tests/resize.test.tsx
  91. 3 3
      src/tests/scene/__snapshots__/export.test.ts.snap
  92. 1 1
      src/tests/scene/export.test.ts
  93. 1 2
      src/tests/selection.test.tsx
  94. 2 2
      src/utils.ts
  95. 0 52
      src/vite-env.d.ts
  96. 1 2
      tsconfig-types.json
  97. 3 3
      tsconfig.json
  98. 0 163
      vite.config.ts
  99. 0 9
      vitest.config.ts
  100. 331 437
      yarn.lock

+ 11 - 11
.env.development

@@ -1,30 +1,30 @@
-VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
-VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
+REACT_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
+REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
 
-VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
-VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
+REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
+REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
 
 # collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
-VITE_APP_WS_SERVER_URL=http://localhost:3002
+REACT_APP_WS_SERVER_URL=http://localhost:3002
 
 # set this only if using the collaboration workflow we use on excalidraw.com
-VITE_APP_PORTAL_URL=
+REACT_APP_PORTAL_URL=
 
-VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
+REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
 
 # put these in your .env.local, or make sure you don't commit!
 # must be lowercase `true` when turned on
 #
 # whether to enable Service Workers in development
-VITE_APP_DEV_ENABLE_SW=
+REACT_APP_DEV_ENABLE_SW=
 # whether to disable live reload / HMR. Usuaully what you want to do when
 # debugging Service Workers.
-VITE_APP_DEV_DISABLE_LIVE_RELOAD=
-VITE_APP_DISABLE_TRACKING=true
+REACT_APP_DEV_DISABLE_LIVE_RELOAD=
+REACT_APP_DISABLE_TRACKING=true
 
 FAST_REFRESH=false
 
 #Debug flags
 
 # To enable bounding box for text containers
-VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
+REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=

+ 8 - 8
.env.production

@@ -1,15 +1,15 @@
 REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
 REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
 
-VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
-VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
+REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
+REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
 
-VITE_APP_PORTAL_URL=https://portal.excalidraw.com
+REACT_APP_PORTAL_URL=https://portal.excalidraw.com
 # Fill to set socket server URL used for collaboration.
-# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
-VITE_APP_WS_SERVER_URL=
+# Meant for forks only: excalidraw.com uses custom REACT_APP_PORTAL_URL flow
+REACT_APP_WS_SERVER_URL=
 
-VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
+REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
 
-VITE_APP_PLUS_APP=https://app.excalidraw.com
-VITE_APP_DISABLE_TRACKING=
+REACT_APP_PLUS_APP=https://app.excalidraw.com
+REACT_APP_DISABLE_TRACKING=

+ 2 - 2
.github/workflows/autorelease-excalidraw.yml

@@ -12,10 +12,10 @@ jobs:
       - uses: actions/checkout@v2
         with:
           fetch-depth: 2
-      - name: Setup Node.js 18.x
+      - name: Setup Node.js 14.x
         uses: actions/setup-node@v2
         with:
-          node-version: 18.x
+          node-version: 14.x
       - name: Set up publish access
         run: |
           npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}

+ 2 - 2
.github/workflows/autorelease-preview.yml

@@ -32,10 +32,10 @@ jobs:
         with:
           ref: ${{ steps.sha.outputs.result }}
           fetch-depth: 2
-      - name: Setup Node.js 18.x
+      - name: Setup Node.js 14.x
         uses: actions/setup-node@v2
         with:
-          node-version: 18.x
+          node-version: 14.x
       - name: Set up publish access
         run: |
           npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}

+ 2 - 2
.github/workflows/lint.yml

@@ -9,10 +9,10 @@ jobs:
     steps:
       - uses: actions/checkout@v2
 
-      - name: Setup Node.js 18.x
+      - name: Setup Node.js 14.x
         uses: actions/setup-node@v2
         with:
-          node-version: 18.x
+          node-version: 14.x
 
       - name: Install and lint
         run: |

+ 2 - 2
.github/workflows/locales-coverage.yml

@@ -14,10 +14,10 @@ jobs:
         with:
           token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
 
-      - name: Setup Node.js 18.x
+      - name: Setup Node.js 14.x
         uses: actions/setup-node@v2
         with:
-          node-version: 18.x
+          node-version: 14.x
 
       - name: Create report file
         run: |

+ 2 - 2
.github/workflows/sentry-production.yml

@@ -10,10 +10,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - name: Setup Node.js 18.x
+      - name: Setup Node.js 14.x
         uses: actions/setup-node@v2
         with:
-          node-version: 18.x
+          node-version: 14.x
       - name: Install and build
         run: |
           yarn --frozen-lockfile

+ 2 - 2
.github/workflows/test.yml

@@ -7,10 +7,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - name: Setup Node.js 18.x
+      - name: Setup Node.js 14.x
         uses: actions/setup-node@v2
         with:
-          node-version: 18.x
+          node-version: 14.x
       - name: Install and test
         run: |
           yarn --frozen-lockfile

+ 0 - 2
.gitignore

@@ -26,5 +26,3 @@ src/packages/excalidraw/example/public/bundle.js
 src/packages/excalidraw/example/public/excalidraw-assets-dev
 src/packages/excalidraw/example/public/excalidraw.development.js
 coverage
-dev-dist
-html

+ 41 - 24
package.json

@@ -32,7 +32,6 @@
     "canvas-roundrect-polyfill": "0.0.1",
     "clsx": "1.1.1",
     "cross-env": "7.0.3",
-    "eslint-plugin-react": "7.32.2",
     "fake-indexeddb": "3.1.7",
     "firebase": "8.3.3",
     "i18next-browser-languagedetector": "6.1.4",
@@ -52,13 +51,26 @@
     "pwacompat": "2.0.17",
     "react": "18.2.0",
     "react-dom": "18.2.0",
+    "react-scripts": "5.0.1",
     "roughjs": "4.5.2",
     "sass": "1.51.0",
     "socket.io-client": "2.3.1",
-    "tunnel-rat": "0.1.2"
+    "tunnel-rat": "0.1.2",
+    "workbox-background-sync": "^6.5.4",
+    "workbox-broadcast-update": "^6.5.4",
+    "workbox-cacheable-response": "^6.5.4",
+    "workbox-core": "^6.5.4",
+    "workbox-expiration": "^6.5.4",
+    "workbox-google-analytics": "^6.5.4",
+    "workbox-navigation-preload": "^6.5.4",
+    "workbox-precaching": "^6.5.4",
+    "workbox-range-requests": "^6.5.4",
+    "workbox-routing": "^6.5.4",
+    "workbox-strategies": "^6.5.4",
+    "workbox-streams": "^6.5.4"
   },
   "devDependencies": {
-    "@excalidraw/eslint-config": "1.0.3",
+    "@excalidraw/eslint-config": "1.0.0",
     "@excalidraw/prettier-config": "1.0.2",
     "@types/chai": "4.3.0",
     "@types/jest": "27.4.0",
@@ -69,42 +81,48 @@
     "@types/react-dom": "18.0.6",
     "@types/resize-observer-browser": "0.1.7",
     "@types/socket.io-client": "1.4.36",
-    "@vitejs/plugin-react": "3.1.0",
-    "@vitest/ui": "0.32.2",
     "chai": "4.3.6",
     "dotenv": "16.0.1",
     "eslint-config-prettier": "8.5.0",
-    "eslint-config-react-app": "7.0.1",
     "eslint-plugin-prettier": "3.3.1",
     "http-server": "14.1.1",
     "husky": "7.0.4",
-    "jsdom": "22.1.0",
+    "jest-canvas-mock": "2.4.0",
     "lint-staged": "12.3.7",
     "pepjs": "0.5.3",
     "prettier": "2.6.2",
     "rewire": "6.0.0",
-    "typescript": "4.9.4",
-    "vite": "4.4.2",
-    "vite-plugin-ejs": "1.6.4",
-    "vite-plugin-eslint": "1.8.1",
-    "vite-plugin-pwa": "0.16.4",
-    "vite-plugin-svgr": "2.4.0",
-    "vitest": "0.32.2",
-    "vitest-canvas-mock": "0.3.2"
+    "typescript": "4.9.4"
   },
   "engines": {
-    "node": ">=18.0.0"
+    "node": ">=14.0.0"
   },
   "homepage": ".",
+  "jest": {
+    "collectCoverageFrom": [
+      "src/**/*.{js,jsx,ts,tsx}"
+    ],
+    "coveragePathIgnorePatterns": [
+      "<rootDir>/locales",
+      "<rootDir>/src/packages/excalidraw/dist/",
+      "<rootDir>/src/packages/excalidraw/types",
+      "<rootDir>/src/packages/excalidraw/example"
+    ],
+    "transformIgnorePatterns": [
+      "node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access|canvas-roundrect-polyfill)/)"
+    ],
+    "resetMocks": false
+  },
   "name": "excalidraw",
   "prettier": "@excalidraw/prettier-config",
   "private": true,
   "scripts": {
     "build-node": "node ./scripts/build-node.js",
-    "build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true vite build",
-    "build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
+    "build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true react-scripts build",
+    "build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
     "build:version": "node ./scripts/build-version.js",
     "build": "yarn build:app && yarn build:version",
+    "eject": "react-scripts eject",
     "fix:code": "yarn test:code --fix",
     "fix:other": "yarn prettier --write",
     "fix": "yarn fix:other && yarn fix:code",
@@ -112,20 +130,19 @@
     "locales-coverage:description": "node scripts/locales-coverage-description.js",
     "prepare": "husky install",
     "prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
-    "start": "vite --port 3000",
+    "start": "react-scripts start",
     "start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
     "test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
-    "test:app": "vitest --config vitest.config.ts",
+    "test:app": "react-scripts test --passWithNoTests",
     "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
+    "test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
     "test:other": "yarn prettier --list-different",
     "test:typecheck": "tsc",
-    "test:update": "yarn test:app --update --watch=false",
+    "test:update": "yarn test:app --updateSnapshot --watchAll=false",
     "test": "yarn test:app",
-    "test:coverage": "vitest --coverage --watchAll",
-    "test:ui": "yarn test --ui",
+    "test:coverage": "react-scripts test --passWithNoTests --coverage --watchAll",
     "autorelease": "node scripts/autorelease.js",
     "prerelease": "node scripts/prerelease.js",
-    "build:preview": "yarn build && vite preview --port 5000",
     "release": "node scripts/release.js"
   }
 }

+ 15 - 9
index.html → public/index.html

@@ -78,7 +78,8 @@
       }
     </style>
     <!------------------------------------------------------------------------->
-    <% if ("%PROD%" === "true") { %>
+
+    <% if (process.env.NODE_ENV === "production") { %>
     <script>
       // Redirect Excalidraw+ users which have auto-redirect enabled.
       //
@@ -99,35 +100,41 @@
     </script>
     <% } %>
 
-    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
+    <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
 
     <!-- Excalidraw version -->
     <meta name="version" content="{version}" />
 
     <link
       rel="preload"
-      href="/Virgil.woff2"
+      href="Virgil.woff2"
       as="font"
       type="font/woff2"
       crossorigin="anonymous"
     />
     <link
       rel="preload"
-      href="/Cascadia.woff2"
+      href="Cascadia.woff2"
       as="font"
       type="font/woff2"
       crossorigin="anonymous"
     />
 
-    <link rel="stylesheet" href="/fonts.css" type="text/css" />
-    <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
+    <link
+      rel="manifest"
+      href="manifest.json"
+      style="--pwacompat-splash-font: 24px Virgil"
+    />
+
+    <link rel="stylesheet" href="fonts.css" type="text/css" />
+    <% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD==="true" ) { %>
     <script>
       {
         const _WebSocket = window.WebSocket;
         window.WebSocket = function (url) {
           if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
             console.info(
-              "[!!!] Live reload is disabled via VITE_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
+              "[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
             );
           } else {
             return new _WebSocket(url);
@@ -193,8 +200,7 @@
       <h1 class="visually-hidden">Excalidraw</h1>
     </header>
     <div id="root"></div>
-    <script type="module" src="/src/index.tsx"></script>
-    <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
+    <% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
     <!-- 100% privacy friendly analytics -->
     <script>
       // need to load this script dynamically bcs. of iframe embed tracking

文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-background-sync.prod.js


+ 2 - 0
public/workbox/workbox-broadcast-update.prod.js

@@ -0,0 +1,2 @@
+this.workbox=this.workbox||{},this.workbox.broadcastUpdate=function(e,t){"use strict";try{self["workbox:broadcast-update:4.3.1"]&&_()}catch(e){}const s=(e,t,s)=>{return!s.some(s=>e.headers.has(s)&&t.headers.has(s))||s.every(s=>{const n=e.headers.has(s)===t.headers.has(s),a=e.headers.get(s)===t.headers.get(s);return n&&a})},n="workbox",a=1e4,i=["content-length","etag","last-modified"],o=async({channel:e,cacheName:t,url:s})=>{const n={type:"CACHE_UPDATED",meta:"workbox-broadcast-update",payload:{cacheName:t,updatedURL:s}};if(e)e.postMessage(n);else{const e=await clients.matchAll({type:"window"});for(const t of e)t.postMessage(n)}};class c{constructor({headersToCheck:e,channelName:t,deferNoticationTimeout:s}={}){this.t=e||i,this.s=t||n,this.i=s||a,this.o()}notifyIfUpdated({oldResponse:e,newResponse:t,url:n,cacheName:a,event:i}){if(!s(e,t,this.t)){const e=(async()=>{i&&i.request&&"navigate"===i.request.mode&&await this.h(i),await this.l({channel:this.u(),cacheName:a,url:n})})();if(i)try{i.waitUntil(e)}catch(e){}return e}}async l(e){await o(e)}u(){return"BroadcastChannel"in self&&!this.p&&(this.p=new BroadcastChannel(this.s)),this.p}h(e){if(!this.m.has(e)){const s=new t.Deferred;this.m.set(e,s);const n=setTimeout(()=>{s.resolve()},this.i);s.promise.then(()=>clearTimeout(n))}return this.m.get(e).promise}o(){this.m=new Map,self.addEventListener("message",e=>{if("WINDOW_READY"===e.data.type&&"workbox-window"===e.data.meta&&this.m.size>0){for(const e of this.m.values())e.resolve();this.m.clear()}})}}return e.BroadcastCacheUpdate=c,e.Plugin=class{constructor(e){this.l=new c(e)}cacheDidUpdate({cacheName:e,oldResponse:t,newResponse:s,request:n,event:a}){t&&this.l.notifyIfUpdated({cacheName:e,oldResponse:t,newResponse:s,event:a,url:n.url})}},e.broadcastUpdate=o,e.responsesAreSame=s,e}({},workbox.core._private);
+//# sourceMappingURL=workbox-broadcast-update.prod.js.map

+ 2 - 0
public/workbox/workbox-cacheable-response.prod.js

@@ -0,0 +1,2 @@
+this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(t){"use strict";try{self["workbox:cacheable-response:4.3.1"]&&_()}catch(t){}class s{constructor(t={}){this.t=t.statuses,this.s=t.headers}isResponseCacheable(t){let s=!0;return this.t&&(s=this.t.includes(t.status)),this.s&&s&&(s=Object.keys(this.s).some(s=>t.headers.get(s)===this.s[s])),s}}return t.CacheableResponse=s,t.Plugin=class{constructor(t){this.i=new s(t)}cacheWillUpdate({response:t}){return this.i.isResponseCacheable(t)?t:null}},t}({});
+//# sourceMappingURL=workbox-cacheable-response.prod.js.map

文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-core.prod.js


文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-expiration.prod.js


+ 2 - 0
public/workbox/workbox-navigation-preload.prod.js

@@ -0,0 +1,2 @@
+this.workbox=this.workbox||{},this.workbox.navigationPreload=function(t){"use strict";try{self["workbox:navigation-preload:4.3.1"]&&_()}catch(t){}function e(){return Boolean(self.registration&&self.registration.navigationPreload)}return t.disable=function(){e()&&self.addEventListener("activate",t=>{t.waitUntil(self.registration.navigationPreload.disable().then(()=>{}))})},t.enable=function(t){e()&&self.addEventListener("activate",e=>{e.waitUntil(self.registration.navigationPreload.enable().then(()=>{t&&self.registration.navigationPreload.setHeaderValue(t)}))})},t.isSupported=e,t}({});
+//# sourceMappingURL=workbox-navigation-preload.prod.js.map

+ 2 - 0
public/workbox/workbox-offline-ga.prod.js

@@ -0,0 +1,2 @@
+this.workbox=this.workbox||{},this.workbox.googleAnalytics=function(e,t,o,n,a,c,w){"use strict";try{self["workbox:google-analytics:4.3.1"]&&_()}catch(e){}const r=/^\/(\w+\/)?collect/,s=e=>async({queue:t})=>{let o;for(;o=await t.shiftRequest();){const{request:n,timestamp:a}=o,c=new URL(n.url);try{const w="POST"===n.method?new URLSearchParams(await n.clone().text()):c.searchParams,r=a-(Number(w.get("qt"))||0),s=Date.now()-r;if(w.set("qt",s),e.parameterOverrides)for(const t of Object.keys(e.parameterOverrides)){const o=e.parameterOverrides[t];w.set(t,o)}"function"==typeof e.hitFilter&&e.hitFilter.call(null,w),await fetch(new Request(c.origin+c.pathname,{body:w.toString(),method:"POST",mode:"cors",credentials:"omit",headers:{"Content-Type":"text/plain"}}))}catch(e){throw await t.unshiftRequest(o),e}}},i=e=>{const t=({url:e})=>"www.google-analytics.com"===e.hostname&&r.test(e.pathname),o=new w.NetworkOnly({plugins:[e]});return[new n.Route(t,o,"GET"),new n.Route(t,o,"POST")]},l=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.google-analytics.com"===e.hostname&&"/analytics.js"===e.pathname,t,"GET")},m=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtag/js"===e.pathname,t,"GET")},u=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtm.js"===e.pathname,t,"GET")};return e.initialize=((e={})=>{const n=o.cacheNames.getGoogleAnalyticsName(e.cacheName),c=new t.Plugin("workbox-google-analytics",{maxRetentionTime:2880,onSync:s(e)}),w=[u(n),l(n),m(n),...i(c)],r=new a.Router;for(const e of w)r.registerRoute(e);r.addFetchListener()}),e}({},workbox.backgroundSync,workbox.core._private,workbox.routing,workbox.routing,workbox.strategies,workbox.strategies);
+//# sourceMappingURL=workbox-offline-ga.prod.js.map

文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-precaching.prod.js


+ 2 - 0
public/workbox/workbox-range-requests.prod.js

@@ -0,0 +1,2 @@
+this.workbox=this.workbox||{},this.workbox.rangeRequests=function(e,n){"use strict";try{self["workbox:range-requests:4.3.1"]&&_()}catch(e){}async function t(e,t){try{if(206===t.status)return t;const s=e.headers.get("range");if(!s)throw new n.WorkboxError("no-range-header");const a=function(e){const t=e.trim().toLowerCase();if(!t.startsWith("bytes="))throw new n.WorkboxError("unit-must-be-bytes",{normalizedRangeHeader:t});if(t.includes(","))throw new n.WorkboxError("single-range-only",{normalizedRangeHeader:t});const s=/(\d*)-(\d*)/.exec(t);if(null===s||!s[1]&&!s[2])throw new n.WorkboxError("invalid-range-values",{normalizedRangeHeader:t});return{start:""===s[1]?null:Number(s[1]),end:""===s[2]?null:Number(s[2])}}(s),r=await t.blob(),i=function(e,t,s){const a=e.size;if(s>a||t<0)throw new n.WorkboxError("range-not-satisfiable",{size:a,end:s,start:t});let r,i;return null===t?(r=a-s,i=a):null===s?(r=t,i=a):(r=t,i=s+1),{start:r,end:i}}(r,a.start,a.end),o=r.slice(i.start,i.end),u=o.size,l=new Response(o,{status:206,statusText:"Partial Content",headers:t.headers});return l.headers.set("Content-Length",u),l.headers.set("Content-Range",`bytes ${i.start}-${i.end-1}/`+r.size),l}catch(e){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}return e.createPartialResponse=t,e.Plugin=class{async cachedResponseWillBeUsed({request:e,cachedResponse:n}){return n&&e.headers.has("range")?await t(e,n):n}},e}({},workbox.core._private);
+//# sourceMappingURL=workbox-range-requests.prod.js.map

文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-routing.prod.js


文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-strategies.prod.js


+ 2 - 0
public/workbox/workbox-streams.prod.js

@@ -0,0 +1,2 @@
+this.workbox=this.workbox||{},this.workbox.streams=function(e){"use strict";try{self["workbox:streams:4.3.1"]&&_()}catch(e){}function n(e){const n=e.map(e=>Promise.resolve(e).then(e=>(function(e){return e.body&&e.body.getReader?e.body.getReader():e.getReader?e.getReader():new Response(e).body.getReader()})(e)));let t,r;const s=new Promise((e,n)=>{t=e,r=n});let o=0;return{done:s,stream:new ReadableStream({pull(e){return n[o].then(e=>e.read()).then(r=>{if(r.done)return++o>=n.length?(e.close(),void t()):this.pull(e);e.enqueue(r.value)}).catch(e=>{throw r(e),e})},cancel(){t()}})}}function t(e={}){const n=new Headers(e);return n.has("content-type")||n.set("content-type","text/html"),n}function r(e,r){const{done:s,stream:o}=n(e),a=t(r);return{done:s,response:new Response(o,{headers:a})}}let s=void 0;function o(){if(void 0===s)try{new ReadableStream({start(){}}),s=!0}catch(e){s=!1}return s}return e.concatenate=n,e.concatenateToResponse=r,e.isSupported=o,e.strategy=function(e,n){return async({event:s,url:a,params:c})=>{if(o()){const{done:t,response:o}=r(e.map(e=>e({event:s,url:a,params:c})),n);return s.waitUntil(t),o}const i=await Promise.all(e.map(e=>e({event:s,url:a,params:c})).map(async e=>{const n=await e;return n instanceof Response?n.blob():n})),u=t(n);return new Response(new Blob(i),{headers:u})}},e}({});
+//# sourceMappingURL=workbox-streams.prod.js.map

+ 2 - 0
public/workbox/workbox-sw.js

@@ -0,0 +1,2 @@
+!function(){"use strict";try{self["workbox:sw:4.3.1"]&&_()}catch(t){}const t="https://storage.googleapis.com/workbox-cdn/releases/4.3.1",e={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams"};self.workbox=new class{constructor(){return this.v={},this.t={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.s=this.t.debug?"dev":"prod",this.o=!1,new Proxy(this,{get(t,s){if(t[s])return t[s];const o=e[s];return o&&t.loadModule(`workbox-${o}`),t[s]}})}setConfig(t={}){if(this.o)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.t,t),this.s=this.t.debug?"dev":"prod"}loadModule(t){const e=this.i(t);try{importScripts(e),this.o=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}i(e){if(this.t.modulePathCb)return this.t.modulePathCb(e,this.t.debug);let s=[t];const o=`${e}.${this.s}.js`,r=this.t.modulePathPrefix;return r&&""===(s=r.split("/"))[s.length-1]&&s.splice(s.length-1,1),s.push(o),s.join("/")}}}();
+//# sourceMappingURL=workbox-sw.js.map

文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-window.prod.es5.mjs


文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-window.prod.mjs


文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-window.prod.umd.js


+ 1 - 1
src/analytics.ts

@@ -11,7 +11,7 @@ export const trackEvent = (
     // Uncomment the next line to track locally
     // console.log("Track Event", { category, action, label, value });
 
-    if (typeof window === "undefined" || import.meta.env.VITE_WORKER_ID) {
+    if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
       return;
     }
 

+ 3 - 2
src/charts.ts

@@ -6,6 +6,7 @@ import {
 import {
   DEFAULT_FONT_FAMILY,
   DEFAULT_FONT_SIZE,
+  ENV,
   VERTICAL_ALIGN,
 } from "./constants";
 import { newElement, newLinearElement, newTextElement } from "./element";
@@ -383,7 +384,7 @@ const chartTypeBar = (
       y,
       groupId,
       backgroundColor,
-      import.meta.env.DEV,
+      process.env.NODE_ENV === ENV.DEVELOPMENT,
     ),
   ];
 };
@@ -472,7 +473,7 @@ const chartTypeLine = (
       y,
       groupId,
       backgroundColor,
-      import.meta.env.DEV,
+      process.env.NODE_ENV === ENV.DEVELOPMENT,
     ),
     line,
     ...lines,

+ 1 - 2
src/components/App.test.tsx

@@ -4,9 +4,8 @@ import { reseed } from "../random";
 import { render, queryByTestId } from "../tests/test-utils";
 
 import ExcalidrawApp from "../excalidraw-app";
-import { vi } from "vitest";
 
-const renderScene = vi.spyOn(Renderer, "renderScene");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 
 describe("Test <App/>", () => {
   beforeEach(async () => {

+ 9 - 4
src/components/App.tsx

@@ -255,7 +255,6 @@ import {
   isTransparent,
   easeToValuesRAF,
   muteFSAbortError,
-  isTestEnv,
   easeOut,
 } from "../utils";
 import {
@@ -1596,7 +1595,10 @@ class App extends React.Component<AppProps, AppState> {
     this.excalidrawContainerValue.container =
       this.excalidrawContainerRef.current;
 
-    if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
+    if (
+      process.env.NODE_ENV === ENV.TEST ||
+      process.env.NODE_ENV === ENV.DEVELOPMENT
+    ) {
       const setState = this.setState.bind(this);
       Object.defineProperties(window.h, {
         state: {
@@ -1634,7 +1636,7 @@ class App extends React.Component<AppProps, AppState> {
       // bounding rects don't work in tests so updating
       // the state on init would result in making the test enviro run
       // in mobile breakpoint (0 width/height), making everything fail
-      !isTestEnv()
+      process.env.NODE_ENV !== "test"
     ) {
       this.refreshDeviceState(this.excalidrawContainerRef.current);
     }
@@ -8160,7 +8162,10 @@ declare global {
   }
 }
 
-if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
+if (
+  process.env.NODE_ENV === ENV.TEST ||
+  process.env.NODE_ENV === ENV.DEVELOPMENT
+) {
   window.h = window.h || ({} as Window["h"]);
 
   Object.defineProperties(window.h, {

+ 1 - 1
src/components/LibraryMenuBrowseButton.tsx

@@ -16,7 +16,7 @@ const LibraryMenuBrowseButton = ({
   return (
     <a
       className="library-menu-browse-button"
-      href={`${import.meta.env.VITE_APP_LIBRARY_URL}?target=${
+      href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
         window.name || "_blank"
       }&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
         VERSIONS.excalidrawLibrary

+ 1 - 1
src/components/PublishLibrary.tsx

@@ -319,7 +319,7 @@ const PublishLibrary = ({
     formData.append("twitterHandle", libraryData.twitterHandle);
     formData.append("website", libraryData.website);
 
-    fetch(`${import.meta.env.VITE_APP_LIBRARY_BACKEND}/submit`, {
+    fetch(`${process.env.REACT_APP_LIBRARY_BACKEND}/submit`, {
       method: "post",
       body: formData,
     })

+ 1 - 2
src/components/Sidebar/Sidebar.test.tsx

@@ -10,7 +10,6 @@ import {
   waitFor,
   withExcalidrawDimensions,
 } from "../../tests/test-utils";
-import { vi } from "vitest";
 
 export const assertSidebarDockButton = async <T extends boolean>(
   hasDockButton: T,
@@ -206,7 +205,7 @@ describe("Sidebar", () => {
     });
 
     it("<Sidebar.Header> should render close button", async () => {
-      const onStateChange = vi.fn();
+      const onStateChange = jest.fn();
       const CustomExcalidraw = () => {
         return (
           <Excalidraw

+ 1 - 1
src/components/Sidebar/Sidebar.tsx

@@ -53,7 +53,7 @@ export const SidebarInner = forwardRef(
     }: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
     ref: React.ForwardedRef<HTMLDivElement>,
   ) => {
-    if (import.meta.env.DEV && onDock && docked == null) {
+    if (process.env.NODE_ENV === "development" && onDock && docked == null) {
       console.warn(
         "Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`",
       );

+ 2 - 2
src/components/__snapshots__/App.test.tsx.snap

@@ -1,6 +1,6 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Test <App/> > should show error modal when using brave and measureText API is not working 1`] = `
+exports[`Test <App/> should show error modal when using brave and measureText API is not working 1`] = `
 <div
   data-testid="brave-measure-text-error"
 >

+ 1 - 1
src/element/newElement.ts

@@ -443,7 +443,7 @@ const _deepCopyElement = (val: any, depth: number = 0) => {
   // we're not cloning non-array & non-plain-object objects because we
   // don't support them on excalidraw elements yet. If we do, we need to make
   // sure we start cloning them, so let's warn about it.
-  if (import.meta.env.DEV) {
+  if (process.env.NODE_ENV === "development") {
     if (
       objectType !== "[object Object]" &&
       objectType !== "[object Array]" &&

+ 1 - 17
src/element/sizeHelpers.test.ts

@@ -1,32 +1,19 @@
-import { vi } from "vitest";
 import { getPerfectElementSize } from "./sizeHelpers";
 import * as constants from "../constants";
 
 const EPSILON_DIGITS = 3;
-// Needed so that we can mock the value of constants which is done in
-// below tests. In Jest this wasn't needed as global override was possible
-// but vite doesn't allow that hence we need to mock
-vi.mock(
-  "../constants.ts",
-  //@ts-ignore
-  async (importOriginal) => {
-    const module: any = await importOriginal();
-    return { ...module };
-  },
-);
+
 describe("getPerfectElementSize", () => {
   it("should return height:0 if `elementType` is line and locked angle is 0", () => {
     const { height, width } = getPerfectElementSize("line", 149, 10);
     expect(width).toBeCloseTo(149, EPSILON_DIGITS);
     expect(height).toBeCloseTo(0, EPSILON_DIGITS);
   });
-
   it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
     const { height, width } = getPerfectElementSize("line", 10, 140);
     expect(width).toBeCloseTo(0, EPSILON_DIGITS);
     expect(height).toBeCloseTo(140, EPSILON_DIGITS);
   });
-
   it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
     const { height, width } = getPerfectElementSize("arrow", 200, 20);
     expect(width).toBeCloseTo(200, EPSILON_DIGITS);
@@ -37,19 +24,16 @@ describe("getPerfectElementSize", () => {
     expect(width).toBeCloseTo(0, EPSILON_DIGITS);
     expect(height).toBeCloseTo(100, EPSILON_DIGITS);
   });
-
   it("should return adjust height to be width * tan(locked angle)", () => {
     const { height, width } = getPerfectElementSize("arrow", 120, 185);
     expect(width).toBeCloseTo(120, EPSILON_DIGITS);
     expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
   });
-
   it("should return height equals to width if locked angle is 45 deg", () => {
     const { height, width } = getPerfectElementSize("arrow", 135, 145);
     expect(width).toBeCloseTo(135, EPSILON_DIGITS);
     expect(height).toBeCloseTo(135, EPSILON_DIGITS);
   });
-
   it("should return height:0 and width:0 when width and height are 0", () => {
     const { height, width } = getPerfectElementSize("arrow", 0, 0);
     expect(width).toBeCloseTo(0, EPSILON_DIGITS);

+ 12 - 12
src/element/textWysiwyg.test.tsx

@@ -955,7 +955,7 @@ describe("textWysiwyg", () => {
       // should center align horizontally and vertically by default
       resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
       expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-        [
+        Array [
           85,
           4.5,
         ]
@@ -979,7 +979,7 @@ describe("textWysiwyg", () => {
       // should left align horizontally and bottom vertically after resize
       resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
       expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-        [
+        Array [
           15,
           65,
         ]
@@ -1001,7 +1001,7 @@ describe("textWysiwyg", () => {
       // should right align horizontally and top vertically after resize
       resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
       expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-        [
+        Array [
           375,
           -539,
         ]
@@ -1279,7 +1279,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Left"));
         fireEvent.click(screen.getByTitle("Align top"));
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          [
+          Array [
             15,
             25,
           ]
@@ -1290,7 +1290,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Center"));
         fireEvent.click(screen.getByTitle("Align top"));
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          [
+          Array [
             30,
             25,
           ]
@@ -1302,7 +1302,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Align top"));
 
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          [
+          Array [
             45,
             25,
           ]
@@ -1313,7 +1313,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Center vertically"));
         fireEvent.click(screen.getByTitle("Left"));
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          [
+          Array [
             15,
             45,
           ]
@@ -1325,7 +1325,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Center vertically"));
 
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          [
+          Array [
             30,
             45,
           ]
@@ -1337,7 +1337,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Center vertically"));
 
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          [
+          Array [
             45,
             45,
           ]
@@ -1349,7 +1349,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Align bottom"));
 
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          [
+          Array [
             15,
             65,
           ]
@@ -1360,7 +1360,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Center"));
         fireEvent.click(screen.getByTitle("Align bottom"));
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          [
+          Array [
             30,
             65,
           ]
@@ -1371,7 +1371,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Right"));
         fireEvent.click(screen.getByTitle("Align bottom"));
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          [
+          Array [
             45,
             65,
           ]

+ 8 - 2
src/excalidraw-app/collab/Collab.tsx

@@ -171,7 +171,10 @@ class Collab extends PureComponent<Props, CollabState> {
 
     appJotaiStore.set(collabAPIAtom, collabAPI);
 
-    if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
+    if (
+      process.env.NODE_ENV === ENV.TEST ||
+      process.env.NODE_ENV === ENV.DEVELOPMENT
+    ) {
       window.collab = window.collab || ({} as Window["collab"]);
       Object.defineProperties(window, {
         collab: {
@@ -857,7 +860,10 @@ declare global {
   }
 }
 
-if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
+if (
+  process.env.NODE_ENV === ENV.TEST ||
+  process.env.NODE_ENV === ENV.DEVELOPMENT
+) {
   window.collab = window.collab || ({} as Window["collab"]);
 }
 

+ 1 - 3
src/excalidraw-app/components/AppWelcomeScreen.tsx

@@ -19,9 +19,7 @@ export const AppWelcomeScreen: React.FC<{
           return (
             <a
               style={{ pointerEvents: "all" }}
-              href={`${
-                import.meta.env.VITE_APP_PLUS_APP
-              }?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
+              href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
               key={idx}
             >
               Excalidraw+

+ 1 - 3
src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx

@@ -6,9 +6,7 @@ export const ExcalidrawPlusAppLink = () => {
   }
   return (
     <a
-      href={`${
-        import.meta.env.VITE_APP_PLUS_APP
-      }?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
+      href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
       target="_blank"
       rel="noreferrer"
       className="plus-button"

+ 2 - 4
src/excalidraw-app/data/firebase.ts

@@ -21,12 +21,10 @@ import { ResolutionType } from "../../utility-types";
 
 let FIREBASE_CONFIG: Record<string, any>;
 try {
-  FIREBASE_CONFIG = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
+  FIREBASE_CONFIG = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
 } catch (error: any) {
   console.warn(
-    `Error JSON parsing firebase config. Supplied value: ${
-      import.meta.env.VITE_APP_FIREBASE_CONFIG
-    }`,
+    `Error JSON parsing firebase config. Supplied value: ${process.env.REACT_APP_FIREBASE_CONFIG}`,
   );
   FIREBASE_CONFIG = {};
 }

+ 5 - 5
src/excalidraw-app/data/index.ts

@@ -47,8 +47,8 @@ export const getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
     isSyncableElement(element),
   ) as SyncableExcalidrawElement[];
 
-const BACKEND_V2_GET = import.meta.env.VITE_APP_BACKEND_V2_GET_URL;
-const BACKEND_V2_POST = import.meta.env.VITE_APP_BACKEND_V2_POST_URL;
+const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
+const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
 
 const generateRoomId = async () => {
   const buffer = new Uint8Array(ROOM_ID_BYTES);
@@ -67,16 +67,16 @@ export const getCollabServer = async (): Promise<{
   url: string;
   polling: boolean;
 }> => {
-  if (import.meta.env.VITE_APP_WS_SERVER_URL) {
+  if (process.env.REACT_APP_WS_SERVER_URL) {
     return {
-      url: import.meta.env.VITE_APP_WS_SERVER_URL,
+      url: process.env.REACT_APP_WS_SERVER_URL,
       polling: true,
     };
   }
 
   try {
     const resp = await fetch(
-      `${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
+      `${process.env.REACT_APP_PORTAL_URL}/collab-server`,
     );
     return await resp.json();
   } catch (error) {

+ 31 - 0
src/excalidraw-app/pwa.ts

@@ -0,0 +1,31 @@
+import { register as registerServiceWorker } from "../serviceWorkerRegistration";
+import { EVENT } from "../constants";
+
+// On Apple mobile devices add the proprietary app icon and splashscreen markup.
+// No one should have to do this manually, and eventually this annoyance will
+// go away once https://bugs.webkit.org/show_bug.cgi?id=183937 is fixed.
+if (
+  /\b(iPad|iPhone|iPod|Safari)\b/.test(navigator.userAgent) &&
+  !matchMedia("(display-mode: standalone)").matches
+) {
+  import(/* webpackChunkName: "pwacompat" */ "pwacompat");
+}
+
+registerServiceWorker({
+  onUpdate: (registration) => {
+    const waitingServiceWorker = registration.waiting;
+    if (waitingServiceWorker) {
+      waitingServiceWorker.addEventListener(
+        EVENT.STATE_CHANGE,
+        (event: Event) => {
+          const target = event.target as ServiceWorker;
+          const state = target.state as ServiceWorkerState;
+          if (state === "activated") {
+            window.location.reload();
+          }
+        },
+      );
+      waitingServiceWorker.postMessage({ type: "SKIP_WAITING" });
+    }
+  },
+});

+ 2 - 2
src/excalidraw-app/sentry.ts

@@ -7,7 +7,7 @@ const SentryEnvHostnameMap: { [key: string]: string } = {
 };
 
 const REACT_APP_DISABLE_SENTRY =
-  import.meta.env.VITE_APP_DISABLE_SENTRY === "true";
+  process.env.REACT_APP_DISABLE_SENTRY === "true";
 
 // Disable Sentry locally or inside the Docker to avoid noise/respect privacy
 const onlineEnv =
@@ -21,7 +21,7 @@ Sentry.init({
     ? "https://[email protected]/5179260"
     : undefined,
   environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
-  release: import.meta.env.VITE_APP_GIT_SHA,
+  release: process.env.REACT_APP_GIT_SHA,
   ignoreErrors: [
     "undefined is not an object (evaluating 'window.__pad.performLoop')", // Only happens on Safari, but spams our servers. Doesn't break anything
   ],

+ 10 - 0
src/global.d.ts

@@ -38,6 +38,16 @@ interface CanvasRenderingContext2D {
   ) => void;
 }
 
+// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts
+declare namespace NodeJS {
+  interface ProcessEnv {
+    readonly REACT_APP_BACKEND_V2_GET_URL: string;
+    readonly REACT_APP_BACKEND_V2_POST_URL: string;
+    readonly REACT_APP_PORTAL_URL: string;
+    readonly REACT_APP_FIREBASE_CONFIG: string;
+  }
+}
+
 interface Clipboard extends EventTarget {
   write(data: any[]): Promise<void>;
 }

+ 3 - 2
src/i18n.ts

@@ -1,5 +1,6 @@
 import fallbackLangData from "./locales/en.json";
 import percentages from "./locales/percentages.json";
+import { ENV } from "./constants";
 import { jotaiScope, jotaiStore } from "./jotai";
 import { atom, useAtomValue } from "jotai";
 import { NestedKeyOf } from "./utility-types";
@@ -73,7 +74,7 @@ export const languages: Language[] = [
 ];
 
 const TEST_LANG_CODE = "__test__";
-if (import.meta.env.DEV) {
+if (process.env.NODE_ENV === ENV.DEVELOPMENT) {
   languages.unshift(
     { code: TEST_LANG_CODE, label: "test language" },
     {
@@ -144,7 +145,7 @@ export const t = (
   if (translation === undefined) {
     const errorMessage = `Can't find translation for ${path}`;
     // in production, don't blow up the app on a missing translation key
-    if (import.meta.env.PROD) {
+    if (process.env.NODE_ENV === "production") {
       console.warn(errorMessage);
       return "";
     }

+ 2 - 1
src/index.tsx

@@ -2,8 +2,9 @@ import { StrictMode } from "react";
 import { createRoot } from "react-dom/client";
 import ExcalidrawApp from "./excalidraw-app";
 
+import "./excalidraw-app/pwa";
 import "./excalidraw-app/sentry";
-window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
+window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
 const rootElement = document.getElementById("root")!;
 const root = createRoot(rootElement);
 root.render(

+ 3 - 3
src/packages/excalidraw/env.js

@@ -9,9 +9,9 @@ const parseEnvVariables = (filepath) => {
     },
     {},
   );
-  envVars.VITE_PKG_NAME = JSON.stringify(pkg.name);
-  envVars.VITE_PKG_VERSION = JSON.stringify(pkg.version);
-  envVars.VITE_IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
+  envVars.PKG_NAME = JSON.stringify(pkg.name);
+  envVars.PKG_VERSION = JSON.stringify(pkg.version);
+  envVars.IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
   return envVars;
 };
 

+ 0 - 1
src/packages/excalidraw/package.json

@@ -59,7 +59,6 @@
     "cross-env": "7.0.3",
     "css-loader": "6.7.1",
     "dotenv": "16.0.1",
-    "import-meta-loader": "1.1.0",
     "mini-css-extract-plugin": "2.6.1",
     "postcss-loader": "7.0.1",
     "sass-loader": "13.0.2",

+ 1 - 1
src/packages/excalidraw/publicPath.js

@@ -4,5 +4,5 @@ if (process.env.NODE_ENV !== ENV.TEST) {
   /* global __webpack_public_path__:writable */
   __webpack_public_path__ =
     window.EXCALIDRAW_ASSET_PATH ||
-    `https://unpkg.com/${process.env.VITE_PKG_NAME}@${process.env.VITE_PKG_VERSION}/dist/`;
+    `https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}/dist/`;
 }

+ 0 - 3
src/packages/excalidraw/webpack.dev.config.js

@@ -47,9 +47,6 @@ module.exports = {
         exclude:
           /node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
         use: [
-          {
-            loader: "import-meta-loader",
-          },
           {
             loader: "ts-loader",
             options: {

+ 0 - 3
src/packages/excalidraw/webpack.prod.config.js

@@ -50,9 +50,6 @@ module.exports = {
           /node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
 
         use: [
-          {
-            loader: "import-meta-loader",
-          },
           {
             loader: "ts-loader",
             options: {

+ 0 - 5
src/packages/excalidraw/yarn.lock

@@ -2916,11 +2916,6 @@ import-local@^3.0.2:
     pkg-dir "^4.2.0"
     resolve-cwd "^3.0.0"
 
[email protected]:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/import-meta-loader/-/import-meta-loader-1.1.0.tgz#927060305f2d0f88b495f2754aa33387ca6579d7"
-  integrity sha512-f96r2o8xT+b2KVlOY4x+1KTJmJiapZlf77j1WebR8NQgMG1dpdqijjGl4i/2jMoXch2CVqcQoTMfh5BR7bR8wA==
-
 indexes-of@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"

+ 1 - 1
src/renderer/renderElement.ts

@@ -915,7 +915,7 @@ const drawElementFromCanvas = (
     );
 
     if (
-      import.meta.env.VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
+      process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
         "true" &&
       hasBoundTextElement(element)
     ) {

+ 3 - 4
src/scene/export.ts

@@ -133,13 +133,12 @@ export const exportToSvg = async (
   }
 
   let assetPath = "https://excalidraw.com/";
+
   // Asset path needs to be determined only when using package
-  if (import.meta.env.VITE_IS_EXCALIDRAW_NPM_PACKAGE) {
+  if (process.env.IS_EXCALIDRAW_NPM_PACKAGE) {
     assetPath =
       window.EXCALIDRAW_ASSET_PATH ||
-      `https://unpkg.com/${import.meta.env.VITE_PKG_NAME}@${
-        import.meta.env.PKG_VERSION
-      }`;
+      `https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}`;
 
     if (assetPath?.startsWith("/")) {
       assetPath = assetPath.replace("/", `${window.location.origin}/`);

+ 147 - 0
src/service-worker.ts

@@ -0,0 +1,147 @@
+/// <reference lib="webworker" />
+/* eslint-disable no-restricted-globals */
+
+// This service worker can be customized!
+// See https://developers.google.com/web/tools/workbox/modules
+// for the list of available Workbox modules, or add any other
+// code you'd like.
+// You can also remove this file if you'd prefer not to use a
+// service worker, and the Workbox build step will be skipped.
+
+import { clientsClaim } from "workbox-core";
+import { ExpirationPlugin } from "workbox-expiration";
+import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
+import { registerRoute } from "workbox-routing";
+import { CacheFirst, StaleWhileRevalidate } from "workbox-strategies";
+
+declare const self: ServiceWorkerGlobalScope;
+
+clientsClaim();
+
+// Precache assets generated by your build process.
+//
+// Their URLs are injected into the __WB_MANIFEST during build (by workbox).
+//
+// This variable must be present somewhere in your service worker file,
+// even if you decide not to use precaching. See https://cra.link/PWA.
+//
+// We don't want to precache i18n files so we filter them out
+// (normally this should be configured in a webpack workbox plugin, but we don't
+// have access to it in CRA) — this is because all users will use at most
+// one or two languages, so there's no point fetching all of them. (They'll
+// be cached as you load them.)
+const manifest = self.__WB_MANIFEST.filter((entry) => {
+  return !/locales\/[\w-]+json/.test(
+    typeof entry === "string" ? entry : entry.url,
+  );
+});
+
+precacheAndRoute(manifest);
+
+// Set up App Shell-style routing, so that all navigation requests
+// are fulfilled with your index.html shell. Learn more at
+// https://developer.chrome.com/docs/workbox/app-shell-model/
+//
+// below is copied verbatim from CRA@5
+const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
+registerRoute(
+  // Return false to exempt requests from being fulfilled by index.html.
+  ({ request, url }: { request: Request; url: URL }) => {
+    // If this isn't a navigation, skip.
+    if (request.mode !== "navigate") {
+      return false;
+    }
+
+    // If this is a URL that starts with /_, skip.
+    if (url.pathname.startsWith("/_")) {
+      return false;
+    }
+
+    // If this looks like a URL for a resource, because it contains
+    // a file extension, skip.
+    if (url.pathname.match(fileExtensionRegexp)) {
+      return false;
+    }
+
+    // Return true to signal that we want to use the handler.
+    return true;
+  },
+  createHandlerBoundToURL(`${process.env.PUBLIC_URL}/index.html`),
+);
+
+// Cache resources that aren't being precached
+// -----------------------------------------------------------------------------
+
+registerRoute(
+  new RegExp("/fonts.css"),
+  new StaleWhileRevalidate({
+    cacheName: "fonts",
+    plugins: [
+      // Ensure that once this runtime cache reaches a maximum size the
+      // least-recently used images are removed.
+      new ExpirationPlugin({ maxEntries: 50 }),
+    ],
+  }),
+);
+
+// since we serve fonts from, don't forget to append new ?v= param when
+// updating fonts (glyphs) without changing the filename
+registerRoute(
+  new RegExp("/.+.(ttf|woff2|otf)"),
+  new CacheFirst({
+    cacheName: "fonts",
+    plugins: [
+      // Ensure that once this runtime cache reaches a maximum size the
+      // least-recently used images are removed.
+      new ExpirationPlugin({
+        maxEntries: 50,
+        // 90 days
+        maxAgeSeconds: 7776000000,
+      }),
+    ],
+  }),
+);
+
+registerRoute(
+  new RegExp("/locales\\/[\\w-]+json"),
+  // Customize this strategy as needed, e.g., by changing to CacheFirst.
+  new CacheFirst({
+    cacheName: "locales",
+    plugins: [
+      // Ensure that once this runtime cache reaches a maximum size the
+      // least-recently used images are removed.
+      new ExpirationPlugin({
+        maxEntries: 50,
+        // 30 days
+        maxAgeSeconds: 2592000000,
+      }),
+    ],
+  }),
+);
+
+// -----------------------------------------------------------------------------
+
+self.addEventListener("fetch", (event) => {
+  if (
+    event.request.method === "POST" &&
+    event.request.url.endsWith("/web-share-target")
+  ) {
+    return event.respondWith(
+      (async () => {
+        const formData = await event.request.formData();
+        const file = formData.get("file");
+        const webShareTargetCache = await caches.open("web-share-target");
+        await webShareTargetCache.put("shared-file", new Response(file));
+        return Response.redirect("/?web-share-target", 303);
+      })(),
+    );
+  }
+});
+
+// This allows the web app to trigger skipWaiting via
+// registration.waiting.postMessage({type: 'SKIP_WAITING'})
+self.addEventListener("message", (event) => {
+  if (event.data && event.data.type === "SKIP_WAITING") {
+    self.skipWaiting();
+  }
+});

+ 162 - 0
src/serviceWorkerRegistration.ts

@@ -0,0 +1,162 @@
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://bit.ly/CRA-PWA
+
+const isLocalhost = Boolean(
+  window.location.hostname === "localhost" ||
+    // [::1] is the IPv6 localhost address.
+    window.location.hostname === "[::1]" ||
+    // 127.0.0.0/8 are considered localhost for IPv4.
+    window.location.hostname.match(
+      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
+    ),
+);
+
+type Config = {
+  onSuccess?: (registration: ServiceWorkerRegistration) => void;
+  onUpdate?: (registration: ServiceWorkerRegistration) => void;
+};
+
+export const register = (config?: Config) => {
+  if (
+    (process.env.NODE_ENV === "production" ||
+      process.env.REACT_APP_DEV_ENABLE_SW?.toLowerCase() === "true") &&
+    "serviceWorker" in navigator
+  ) {
+    // The URL constructor is available in all browsers that support SW.
+    const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+    if (publicUrl.origin !== window.location.origin) {
+      // Our service worker won't work if PUBLIC_URL is on a different origin
+      // from what our page is served on. This might happen if a CDN is used to
+      // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+      return;
+    }
+
+    window.addEventListener("load", () => {
+      const isWebexLP = window.location.pathname.startsWith("/webex");
+      if (isWebexLP) {
+        unregister(() => {
+          window.location.reload();
+        });
+        return false;
+      }
+      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+      if (isLocalhost) {
+        // This is running on localhost. Let's check if a service worker still exists or not.
+        checkValidServiceWorker(swUrl, config);
+
+        // Add some additional logging to localhost, pointing developers to the
+        // service worker/PWA documentation.
+        navigator.serviceWorker.ready.then(() => {
+          console.info(
+            "This web app is being served cache-first by a service " +
+              "worker. To learn more, visit https://bit.ly/CRA-PWA",
+          );
+        });
+      } else {
+        // Is not localhost. Just register service worker
+        registerValidSW(swUrl, config);
+      }
+    });
+  }
+};
+
+const registerValidSW = (swUrl: string, config?: Config) => {
+  navigator.serviceWorker
+    .register(swUrl)
+    .then((registration) => {
+      registration.onupdatefound = () => {
+        const installingWorker = registration.installing;
+        if (installingWorker == null) {
+          return;
+        }
+        installingWorker.onstatechange = () => {
+          if (installingWorker.state === "installed") {
+            if (navigator.serviceWorker.controller) {
+              // At this point, the updated precached content has been fetched,
+              // but the previous service worker will still serve the older
+              // content until all client tabs are closed.
+
+              console.info(
+                "New content is available and will be used when all tabs for this page are closed.",
+              );
+
+              // Execute callback
+              if (config && config.onUpdate) {
+                config.onUpdate(registration);
+              }
+            } else {
+              // At this point, everything has been precached.
+              // It's the perfect time to display a
+              // "Content is cached for offline use." message.
+
+              console.info("Content is cached for offline use.");
+
+              // Execute callback
+              if (config && config.onSuccess) {
+                config.onSuccess(registration);
+              }
+            }
+          }
+        };
+      };
+    })
+    .catch((error) => {
+      console.error("Error during service worker registration:", error);
+    });
+};
+
+const checkValidServiceWorker = (swUrl: string, config?: Config) => {
+  // Check if the service worker can be found. If it can't reload the page.
+  fetch(swUrl, {
+    headers: { "Service-Worker": "script" },
+  })
+    .then((response) => {
+      // Ensure service worker exists, and that we really are getting a JS file.
+      const contentType = response.headers.get("content-type");
+      if (
+        response.status === 404 ||
+        (contentType != null && contentType.indexOf("javascript") === -1)
+      ) {
+        // No service worker found. Probably a different app. Reload the page.
+        navigator.serviceWorker.ready.then((registration) => {
+          registration.unregister().then(() => {
+            window.location.reload();
+          });
+        });
+      } else {
+        // Service worker found. Proceed as normal.
+        registerValidSW(swUrl, config);
+      }
+    })
+    .catch((error) => {
+      console.info(
+        "No internet connection found. App is running in offline mode.",
+        error.message,
+      );
+    });
+};
+
+export const unregister = (callback?: () => void) => {
+  if ("serviceWorker" in navigator) {
+    navigator.serviceWorker.ready
+      .then((registration) => {
+        return registration.unregister();
+      })
+      .then(() => {
+        callback?.();
+      })
+      .catch((error) => {
+        console.error(error.message);
+      });
+  }
+};

+ 8 - 5
src/setupTests.ts

@@ -1,16 +1,19 @@
-// vitest.setup.ts
-import "vitest-canvas-mock";
 import "@testing-library/jest-dom";
-import { vi } from "vitest";
+import "jest-canvas-mock";
+import dotenv from "dotenv";
 import polyfill from "./polyfill";
 
 require("fake-indexeddb/auto");
 
 polyfill();
+// jest doesn't know of .env.development so we need to init it ourselves
+dotenv.config({
+  path: require("path").resolve(__dirname, "../.env.development"),
+});
 
-vi.mock("nanoid", () => {
+jest.mock("nanoid", () => {
   return {
-    nanoid: vi.fn(() => "test-id"),
+    nanoid: jest.fn(() => "test-id"),
   };
 });
 // ReactDOM is located inside index.tsx file

+ 6 - 5
src/tests/MobileMenu.test.tsx

@@ -11,23 +11,23 @@ describe("Test MobileMenu", () => {
   const { h } = window;
   const dimensions = { height: 400, width: 800 };
 
-  beforeAll(() => {
-    mockBoundingClientRect(dimensions);
-  });
-
   beforeEach(async () => {
     await render(<ExcalidrawApp />);
     //@ts-ignore
     h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
   });
 
+  beforeAll(() => {
+    mockBoundingClientRect(dimensions);
+  });
+
   afterAll(() => {
     restoreOriginalGetBoundingClientRect();
   });
 
   it("should set device correctly", () => {
     expect(h.app.device).toMatchInlineSnapshot(`
-      {
+      Object {
         "canDeviceFitSidebar": false,
         "isLandscape": true,
         "isMobile": true,
@@ -39,6 +39,7 @@ describe("Test MobileMenu", () => {
 
   it("should initialize with welcome screen and hide once user interacts", async () => {
     expect(document.querySelector(".welcome-screen-center")).toMatchSnapshot();
+
     UI.clickTool("rectangle");
     expect(document.querySelector(".welcome-screen-center")).toBeNull();
   });

+ 2 - 2
src/tests/__snapshots__/MobileMenu.test.tsx.snap

@@ -1,6 +1,6 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Test MobileMenu > should initialize with welcome screen and hide once user interacts 1`] = `
+exports[`Test MobileMenu should initialize with welcome screen and hide once user interacts 1`] = `
 <div
   class="welcome-screen-center"
 >

+ 6 - 6
src/tests/__snapshots__/charts.test.tsx.snap

@@ -1,15 +1,15 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`tryParseSpreadsheet > works for numbers with comma in them 1`] = `
-{
-  "spreadsheet": {
-    "labels": [
+exports[`tryParseSpreadsheet works for numbers with comma in them 1`] = `
+Object {
+  "spreadsheet": Object {
+    "labels": Array [
       "Week 1",
       "Week 2",
       "Week 3",
     ],
     "title": "Users",
-    "values": [
+    "values": Array [
       814,
       10301,
       4264,

文件差异内容过多而无法显示
+ 183 - 183
src/tests/__snapshots__/contextmenu.test.tsx.snap


+ 31 - 31
src/tests/__snapshots__/dragCreate.test.tsx.snap

@@ -1,9 +1,9 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 1`] = `1`;
+exports[`Test dragCreate add element to the scene when pointer dragging long enough arrow 1`] = `1`;
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 2`] = `
-{
+exports[`Test dragCreate add element to the scene when pointer dragging long enough arrow 2`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -11,7 +11,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -19,18 +19,18 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [
-    [
+  "points": Array [
+    Array [
       0,
       0,
     ],
-    [
+    Array [
       30,
       50,
     ],
   ],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 337897,
@@ -49,16 +49,16 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
 }
 `;
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 1`] = `1`;
+exports[`Test dragCreate add element to the scene when pointer dragging long enough diamond 1`] = `1`;
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 2`] = `
-{
+exports[`Test dragCreate add element to the scene when pointer dragging long enough diamond 2`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -66,7 +66,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 337897,
@@ -83,16 +83,16 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
 }
 `;
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 1`] = `1`;
+exports[`Test dragCreate add element to the scene when pointer dragging long enough ellipse 1`] = `1`;
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 2`] = `
-{
+exports[`Test dragCreate add element to the scene when pointer dragging long enough ellipse 2`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -100,7 +100,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 337897,
@@ -117,8 +117,8 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
 }
 `;
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > line 1`] = `
-{
+exports[`Test dragCreate add element to the scene when pointer dragging long enough line 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -126,7 +126,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -134,18 +134,18 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [
-    [
+  "points": Array [
+    Array [
       0,
       0,
     ],
-    [
+    Array [
       30,
       50,
     ],
   ],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 337897,
@@ -164,16 +164,16 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
 }
 `;
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 1`] = `1`;
+exports[`Test dragCreate add element to the scene when pointer dragging long enough rectangle 1`] = `1`;
 
-exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 2`] = `
-{
+exports[`Test dragCreate add element to the scene when pointer dragging long enough rectangle 2`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -181,7 +181,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": 337897,

文件差异内容过多而无法显示
+ 2 - 2
src/tests/__snapshots__/export.test.tsx.snap


+ 3 - 3
src/tests/__snapshots__/linearElementEditor.test.tsx.snap

@@ -1,11 +1,11 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Test Linear Elements > Test bound text element > should match styles for text editor 1`] = `
+exports[`Test Linear Elements Test bound text element should match styles for text editor 1`] = `
 <textarea
   class="excalidraw-wysiwyg"
   data-type="wysiwyg"
   dir="auto"
-  style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
+  style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
   tabindex="0"
   wrap="off"
 />

+ 34 - 34
src/tests/__snapshots__/move.test.tsx.snap

@@ -1,13 +1,13 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`duplicate element on move when ALT is clicked > rectangle 1`] = `
-{
+exports[`duplicate element on move when ALT is clicked rectangle 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0_copy",
   "isDeleted": false,
@@ -15,7 +15,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 1`] = `
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": 401146281,
@@ -32,14 +32,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 1`] = `
 }
 `;
 
-exports[`duplicate element on move when ALT is clicked > rectangle 2`] = `
-{
+exports[`duplicate element on move when ALT is clicked rectangle 2`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -47,7 +47,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 2`] = `
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": 337897,
@@ -64,14 +64,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 2`] = `
 }
 `;
 
-exports[`move element > rectangle 1`] = `
-{
+exports[`move element rectangle 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -79,7 +79,7 @@ exports[`move element > rectangle 1`] = `
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": 337897,
@@ -96,19 +96,19 @@ exports[`move element > rectangle 1`] = `
 }
 `;
 
-exports[`move element > rectangles with binding arrow 1`] = `
-{
+exports[`move element rectangles with binding arrow 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": [
-    {
+  "boundElements": Array [
+    Object {
       "id": "id2",
       "type": "arrow",
     },
   ],
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 100,
   "id": "id0",
   "isDeleted": false,
@@ -116,7 +116,7 @@ exports[`move element > rectangles with binding arrow 1`] = `
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": 337897,
@@ -133,19 +133,19 @@ exports[`move element > rectangles with binding arrow 1`] = `
 }
 `;
 
-exports[`move element > rectangles with binding arrow 2`] = `
-{
+exports[`move element rectangles with binding arrow 2`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": [
-    {
+  "boundElements": Array [
+    Object {
       "id": "id2",
       "type": "arrow",
     },
   ],
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 300,
   "id": "id1",
   "isDeleted": false,
@@ -153,7 +153,7 @@ exports[`move element > rectangles with binding arrow 2`] = `
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": 449462985,
@@ -170,20 +170,20 @@ exports[`move element > rectangles with binding arrow 2`] = `
 }
 `;
 
-exports[`move element > rectangles with binding arrow 3`] = `
-{
+exports[`move element rectangles with binding arrow 3`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "endArrowhead": null,
-  "endBinding": {
+  "endBinding": Object {
     "elementId": "id1",
     "focus": -0.46666666666666673,
     "gap": 10,
   },
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 81.48231043525051,
   "id": "id2",
   "isDeleted": false,
@@ -191,23 +191,23 @@ exports[`move element > rectangles with binding arrow 3`] = `
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [
-    [
+  "points": Array [
+    Array [
       0,
       0,
     ],
-    [
+    Array [
       81,
       81.48231043525051,
     ],
   ],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 401146281,
   "startArrowhead": null,
-  "startBinding": {
+  "startBinding": Object {
     "elementId": "id0",
     "focus": -0.6000000000000001,
     "gap": 10,

+ 19 - 19
src/tests/__snapshots__/multiPointCreate.test.tsx.snap

@@ -1,7 +1,7 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`multi point mode in linear elements > arrow 1`] = `
-{
+exports[`multi point mode in linear elements arrow 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -9,33 +9,33 @@ exports[`multi point mode in linear elements > arrow 1`] = `
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 110,
   "id": "id0",
   "isDeleted": false,
-  "lastCommittedPoint": [
+  "lastCommittedPoint": Array [
     70,
     110,
   ],
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [
-    [
+  "points": Array [
+    Array [
       0,
       0,
     ],
-    [
+    Array [
       20,
       30,
     ],
-    [
+    Array [
       70,
       110,
     ],
   ],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 337897,
@@ -54,8 +54,8 @@ exports[`multi point mode in linear elements > arrow 1`] = `
 }
 `;
 
-exports[`multi point mode in linear elements > line 1`] = `
-{
+exports[`multi point mode in linear elements line 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -63,33 +63,33 @@ exports[`multi point mode in linear elements > line 1`] = `
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 110,
   "id": "id0",
   "isDeleted": false,
-  "lastCommittedPoint": [
+  "lastCommittedPoint": Array [
     70,
     110,
   ],
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [
-    [
+  "points": Array [
+    Array [
       0,
       0,
     ],
-    [
+    Array [
       20,
       30,
     ],
-    [
+    Array [
       70,
       110,
     ],
   ],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 337897,

文件差异内容过多而无法显示
+ 182 - 182
src/tests/__snapshots__/regressionTests.test.tsx.snap


+ 27 - 27
src/tests/__snapshots__/selection.test.tsx.snap

@@ -1,7 +1,7 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`select single element on the scene > arrow 1`] = `
-{
+exports[`select single element on the scene arrow 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -9,7 +9,7 @@ exports[`select single element on the scene > arrow 1`] = `
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -17,18 +17,18 @@ exports[`select single element on the scene > arrow 1`] = `
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [
-    [
+  "points": Array [
+    Array [
       0,
       0,
     ],
-    [
+    Array [
       30,
       50,
     ],
   ],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 337897,
@@ -47,8 +47,8 @@ exports[`select single element on the scene > arrow 1`] = `
 }
 `;
 
-exports[`select single element on the scene > arrow escape 1`] = `
-{
+exports[`select single element on the scene arrow escape 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -56,7 +56,7 @@ exports[`select single element on the scene > arrow escape 1`] = `
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -64,18 +64,18 @@ exports[`select single element on the scene > arrow escape 1`] = `
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [
-    [
+  "points": Array [
+    Array [
       0,
       0,
     ],
-    [
+    Array [
       30,
       50,
     ],
   ],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 337897,
@@ -94,14 +94,14 @@ exports[`select single element on the scene > arrow escape 1`] = `
 }
 `;
 
-exports[`select single element on the scene > diamond 1`] = `
-{
+exports[`select single element on the scene diamond 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -109,7 +109,7 @@ exports[`select single element on the scene > diamond 1`] = `
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 337897,
@@ -126,14 +126,14 @@ exports[`select single element on the scene > diamond 1`] = `
 }
 `;
 
-exports[`select single element on the scene > ellipse 1`] = `
-{
+exports[`select single element on the scene ellipse 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -141,7 +141,7 @@ exports[`select single element on the scene > ellipse 1`] = `
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": 337897,
@@ -158,14 +158,14 @@ exports[`select single element on the scene > ellipse 1`] = `
 }
 `;
 
-exports[`select single element on the scene > rectangle 1`] = `
-{
+exports[`select single element on the scene rectangle 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -173,7 +173,7 @@ exports[`select single element on the scene > rectangle 1`] = `
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": 337897,

+ 0 - 1
src/tests/appState.test.tsx

@@ -1,5 +1,4 @@
 import { queryByTestId, render, waitFor } from "./test-utils";
-
 import ExcalidrawApp from "../excalidraw-app";
 import { API } from "./helpers/api";
 import { getDefaultAppState } from "../appState";

+ 4 - 5
src/tests/clipboard.test.tsx

@@ -1,4 +1,3 @@
-import { vi } from "vitest";
 import ReactDOM from "react-dom";
 import {
   render,
@@ -22,14 +21,14 @@ const { h } = window;
 
 const mouse = new Pointer("mouse");
 
-vi.mock("../keys.ts", async (importOriginal) => {
-  const module: any = await importOriginal();
+jest.mock("../keys.ts", () => {
+  const actual = jest.requireActual("../keys.ts");
   return {
     __esmodule: true,
-    ...module,
+    ...actual,
     isDarwin: false,
     KEYS: {
-      ...module.KEYS,
+      ...actual.KEYS,
       CTRL_OR_CMD: "ctrlKey",
     },
   };

+ 17 - 23
src/tests/collab.test.tsx

@@ -1,4 +1,3 @@
-import { vi } from "vitest";
 import { render, updateSceneData, waitFor } from "./test-utils";
 import ExcalidrawApp from "../excalidraw-app";
 import { API } from "./helpers/api";
@@ -16,18 +15,15 @@ Object.defineProperty(window, "crypto", {
   },
 });
 
-vi.mock("../excalidraw-app/data/index.ts", async (importActual) => {
-  const module = (await importActual()) as any;
-  return {
-    __esmodule: true,
-    ...module,
-    getCollabServer: vi.fn(() => ({
-      url: /* doesn't really matter */ "http://localhost:3002",
-    })),
-  };
-});
+jest.mock("../excalidraw-app/data/index.ts", () => ({
+  __esmodule: true,
+  ...jest.requireActual("../excalidraw-app/data/index.ts"),
+  getCollabServer: jest.fn(() => ({
+    url: /* doesn't really matter */ "http://localhost:3002",
+  })),
+}));
 
-vi.mock("../excalidraw-app/data/firebase.ts", () => {
+jest.mock("../excalidraw-app/data/firebase.ts", () => {
   const loadFromFirebase = async () => null;
   const saveToFirebase = () => {};
   const isSavedToFirebase = () => true;
@@ -49,17 +45,15 @@ vi.mock("../excalidraw-app/data/firebase.ts", () => {
   };
 });
 
-vi.mock("socket.io-client", () => {
-  return {
-    default: () => {
-      return {
-        close: () => {},
-        on: () => {},
-        once: () => {},
-        off: () => {},
-        emit: () => {},
-      };
-    },
+jest.mock("socket.io-client", () => {
+  return () => {
+    return {
+      close: () => {},
+      on: () => {},
+      once: () => {},
+      off: () => {},
+      emit: () => {},
+    };
   };
 });
 

+ 1 - 2
src/tests/contextmenu.test.tsx

@@ -21,7 +21,6 @@ import { copiedStyles } from "../actions/actionStyles";
 import { API } from "./helpers/api";
 import { setDateTimeForTests } from "../utils";
 import { LibraryItem } from "../types";
-import { vi } from "vitest";
 
 const checkpoint = (name: string) => {
   expect(renderScene.mock.calls.length).toMatchSnapshot(
@@ -40,7 +39,7 @@ const mouse = new Pointer("mouse");
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = vi.spyOn(Renderer, "renderScene");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

+ 57 - 57
src/tests/data/__snapshots__/restore.test.ts.snap

@@ -1,15 +1,15 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`restoreElements > should restore arrow element correctly 1`] = `
-{
+exports[`restoreElements should restore arrow element correctly 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": [],
+  "boundElements": Array [],
   "endArrowhead": null,
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 100,
   "id": "id-arrow01",
   "isDeleted": false,
@@ -17,18 +17,18 @@ exports[`restoreElements > should restore arrow element correctly 1`] = `
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [
-    [
+  "points": Array [
+    Array [
       0,
       0,
     ],
-    [
+    Array [
       100,
       100,
     ],
   ],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": Any<Number>,
@@ -47,14 +47,14 @@ exports[`restoreElements > should restore arrow element correctly 1`] = `
 }
 `;
 
-exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 1`] = `
-{
+exports[`restoreElements should restore correctly with rectangle, ellipse and diamond elements 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "blue",
-  "boundElements": [],
+  "boundElements": Array [],
   "fillStyle": "cross-hatch",
   "frameId": null,
-  "groupIds": [
+  "groupIds": Array [
     "1",
     "2",
     "3",
@@ -66,7 +66,7 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
   "locked": false,
   "opacity": 10,
   "roughness": 2,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": Any<Number>,
@@ -83,14 +83,14 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
 }
 `;
 
-exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 2`] = `
-{
+exports[`restoreElements should restore correctly with rectangle, ellipse and diamond elements 2`] = `
+Object {
   "angle": 0,
   "backgroundColor": "blue",
-  "boundElements": [],
+  "boundElements": Array [],
   "fillStyle": "cross-hatch",
   "frameId": null,
-  "groupIds": [
+  "groupIds": Array [
     "1",
     "2",
     "3",
@@ -102,7 +102,7 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
   "locked": false,
   "opacity": 10,
   "roughness": 2,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": Any<Number>,
@@ -119,14 +119,14 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
 }
 `;
 
-exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 3`] = `
-{
+exports[`restoreElements should restore correctly with rectangle, ellipse and diamond elements 3`] = `
+Object {
   "angle": 0,
   "backgroundColor": "blue",
-  "boundElements": [],
+  "boundElements": Array [],
   "fillStyle": "cross-hatch",
   "frameId": null,
-  "groupIds": [
+  "groupIds": Array [
     "1",
     "2",
     "3",
@@ -138,7 +138,7 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
   "locked": false,
   "opacity": 10,
   "roughness": 2,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": Any<Number>,
@@ -155,14 +155,14 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
 }
 `;
 
-exports[`restoreElements > should restore freedraw element correctly 1`] = `
-{
+exports[`restoreElements should restore freedraw element correctly 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": [],
+  "boundElements": Array [],
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 0,
   "id": "id-freedraw01",
   "isDeleted": false,
@@ -170,10 +170,10 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [],
-  "pressures": [],
+  "points": Array [],
+  "pressures": Array [],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": Any<Number>,
@@ -191,16 +191,16 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
 }
 `;
 
-exports[`restoreElements > should restore line and draw elements correctly 1`] = `
-{
+exports[`restoreElements should restore line and draw elements correctly 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": [],
+  "boundElements": Array [],
   "endArrowhead": null,
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 100,
   "id": "id-line01",
   "isDeleted": false,
@@ -208,18 +208,18 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] =
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [
-    [
+  "points": Array [
+    Array [
       0,
       0,
     ],
-    [
+    Array [
       100,
       100,
     ],
   ],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": Any<Number>,
@@ -238,16 +238,16 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] =
 }
 `;
 
-exports[`restoreElements > should restore line and draw elements correctly 2`] = `
-{
+exports[`restoreElements should restore line and draw elements correctly 2`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": [],
+  "boundElements": Array [],
   "endArrowhead": null,
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 100,
   "id": "id-draw01",
   "isDeleted": false,
@@ -255,18 +255,18 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": [
-    [
+  "points": Array [
+    Array [
       0,
       0,
     ],
-    [
+    Array [
       100,
       100,
     ],
   ],
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 2,
   },
   "seed": Any<Number>,
@@ -285,18 +285,18 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
 }
 `;
 
-exports[`restoreElements > should restore text element correctly passing value for each attribute 1`] = `
-{
+exports[`restoreElements should restore text element correctly passing value for each attribute 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "baseline": 0,
-  "boundElements": [],
+  "boundElements": Array [],
   "containerId": null,
   "fillStyle": "hachure",
   "fontFamily": 1,
   "fontSize": 14,
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 100,
   "id": "id-text01",
   "isDeleted": false,
@@ -306,7 +306,7 @@ exports[`restoreElements > should restore text element correctly passing value f
   "opacity": 100,
   "originalText": "text",
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": Any<Number>,
@@ -326,18 +326,18 @@ exports[`restoreElements > should restore text element correctly passing value f
 }
 `;
 
-exports[`restoreElements > should restore text element correctly with unknown font family, null text and undefined alignment 1`] = `
-{
+exports[`restoreElements should restore text element correctly with unknown font family, null text and undefined alignment 1`] = `
+Object {
   "angle": 0,
   "backgroundColor": "transparent",
   "baseline": 0,
-  "boundElements": [],
+  "boundElements": Array [],
   "containerId": null,
   "fillStyle": "hachure",
   "fontFamily": 1,
   "fontSize": 10,
   "frameId": null,
-  "groupIds": [],
+  "groupIds": Array [],
   "height": 100,
   "id": "id-text01",
   "isDeleted": false,
@@ -347,7 +347,7 @@ exports[`restoreElements > should restore text element correctly with unknown fo
   "opacity": 100,
   "originalText": "test",
   "roughness": 1,
-  "roundness": {
+  "roundness": Object {
     "type": 3,
   },
   "seed": Any<Number>,

+ 5 - 6
src/tests/data/restore.test.ts

@@ -12,10 +12,9 @@ import { ImportedDataState } from "../../data/types";
 import { NormalizedZoomValue } from "../../types";
 import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "../../constants";
 import { newElementWith } from "../../element/mutateElement";
-import { vi } from "vitest";
 
 describe("restoreElements", () => {
-  const mockSizeHelper = vi.spyOn(sizeHelpers, "isInvisiblySmallElement");
+  const mockSizeHelper = jest.spyOn(sizeHelpers, "isInvisiblySmallElement");
 
   beforeEach(() => {
     mockSizeHelper.mockReset();
@@ -153,7 +152,7 @@ describe("restoreElements", () => {
   it("when arrow element has undefined endArrowHead", () => {
     const arrowElement = API.createElement({ type: "arrow" });
     Object.defineProperty(arrowElement, "endArrowhead", {
-      get: vi.fn(() => undefined),
+      get: jest.fn(() => undefined),
     });
 
     const restoredElements = restore.restoreElements([arrowElement], null);
@@ -206,7 +205,7 @@ describe("restoreElements", () => {
       [1, 1],
     ];
     Object.defineProperty(lineElement_0, "points", {
-      get: vi.fn(() => pointsEl_0),
+      get: jest.fn(() => pointsEl_0),
     });
 
     const pointsEl_1 = [
@@ -214,7 +213,7 @@ describe("restoreElements", () => {
       [5, 6],
     ];
     Object.defineProperty(lineElement_1, "points", {
-      get: vi.fn(() => pointsEl_1),
+      get: jest.fn(() => pointsEl_1),
     });
 
     const restoredElements = restore.restoreElements(
@@ -441,7 +440,7 @@ describe("restoreAppState", () => {
       const stubImportedAppState = getDefaultAppState();
 
       Object.defineProperty(stubImportedAppState, "zoom", {
-        get: vi.fn(() => null),
+        get: jest.fn(() => null),
       });
 
       const stubLocalAppState = getDefaultAppState();

+ 1 - 2
src/tests/dragCreate.test.tsx

@@ -10,12 +10,11 @@ import {
 } from "./test-utils";
 import { ExcalidrawLinearElement } from "../element/types";
 import { reseed } from "../random";
-import { vi } from "vitest";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = vi.spyOn(Renderer, "renderScene");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

+ 2 - 3
src/tests/fitToContent.test.tsx

@@ -2,7 +2,6 @@ import { render } from "./test-utils";
 import { API } from "./helpers/api";
 
 import ExcalidrawApp from "../excalidraw-app";
-import { vi } from "vitest";
 
 const { h } = window;
 
@@ -98,11 +97,11 @@ const waitForNextAnimationFrame = () => {
 
 describe("fitToContent animated", () => {
   beforeEach(() => {
-    vi.spyOn(window, "requestAnimationFrame");
+    jest.spyOn(window, "requestAnimationFrame");
   });
 
   afterEach(() => {
-    vi.restoreAllMocks();
+    jest.restoreAllMocks();
   });
 
   it("should ease scroll the viewport to the selected element", async () => {

+ 14 - 13
src/tests/flip.test.tsx

@@ -20,21 +20,21 @@ import ExcalidrawApp from "../excalidraw-app";
 import { mutateElement } from "../element/mutateElement";
 import { NormalizedZoomValue } from "../types";
 import { ROUNDNESS } from "../constants";
-import { vi } from "vitest";
-import * as blob from "../data/blob";
 
 const { h } = window;
-const mouse = new Pointer("mouse");
-// This needs to fixed in vitest mock, as when importActual used with mock
-// the tests hangs - https://github.com/vitest-dev/vitest/issues/546.
-// But fortunately spying and mocking the return value of spy works :p
-
-const resizeImageFileSpy = vi.spyOn(blob, "resizeImageFile");
-const generateIdFromFileSpy = vi.spyOn(blob, "generateIdFromFile");
-
-resizeImageFileSpy.mockImplementation(async (imageFile: File) => imageFile);
-generateIdFromFileSpy.mockImplementation(async () => "fileId" as FileId);
 
+const mouse = new Pointer("mouse");
+jest.mock("../data/blob", () => {
+  const originalModule = jest.requireActual("../data/blob");
+
+  //Prevent Node.js modules errors (document is not defined etc...)
+  return {
+    __esModule: true,
+    ...originalModule,
+    resizeImageFile: (imageFile: File) => imageFile,
+    generateIdFromFile: () => "fileId" as FileId,
+  };
+});
 beforeEach(async () => {
   // Unmount ReactDOM from root
   ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -42,7 +42,7 @@ beforeEach(async () => {
   mouse.reset();
   localStorage.clear();
   sessionStorage.clear();
-  vi.clearAllMocks();
+  jest.clearAllMocks();
 
   Object.assign(document, {
     elementFromPoint: () => GlobalTestState.canvas,
@@ -732,6 +732,7 @@ describe("image", () => {
   it("flips an unrotated image horizontally correctly", async () => {
     //paste image
     await createImage();
+
     await waitFor(() => {
       expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]);
       expect(API.getSelectedElements().length).toBeGreaterThan(0);

+ 5 - 10
src/tests/library.test.tsx

@@ -1,4 +1,3 @@
-import { vi } from "vitest";
 import { fireEvent, render, waitFor } from "./test-utils";
 import { queryByTestId } from "@testing-library/react";
 
@@ -30,15 +29,11 @@ const mockLibraryFilePromise = new Promise<Blob>(async (resolve, reject) => {
   }
 });
 
-vi.mock("../data/filesystem.ts", async (importOriginal) => {
-  const module = await importOriginal();
-  return {
-    __esmodule: true,
-    //@ts-ignore
-    ...module,
-    fileOpen: vi.fn(() => mockLibraryFilePromise),
-  };
-});
+jest.mock("../data/filesystem.ts", () => ({
+  __esmodule: true,
+  ...jest.requireActual("../data/filesystem.ts"),
+  fileOpen: jest.fn(() => mockLibraryFilePromise),
+}));
 
 describe("library", () => {
   beforeEach(async () => {

+ 150 - 151
src/tests/linearElementEditor.test.tsx

@@ -24,9 +24,8 @@ import {
 } from "../element/textElement";
 import * as textElementUtils from "../element/textElement";
 import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
-import { vi } from "vitest";
 
-const renderScene = vi.spyOn(Renderer, "renderScene");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 
 const { h } = window;
 const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
@@ -180,16 +179,16 @@ describe("Test Linear Elements", () => {
     expect(renderScene).toHaveBeenCalledTimes(11);
     expect(line.points.length).toEqual(3);
     expect(line.points).toMatchInlineSnapshot(`
-      [
-        [
+      Array [
+        Array [
           0,
           0,
         ],
-        [
+        Array [
           70,
           50,
         ],
-        [
+        Array [
           40,
           0,
         ],
@@ -274,16 +273,16 @@ describe("Test Linear Elements", () => {
 
       expect(line.points.length).toEqual(3);
       expect(line.points).toMatchInlineSnapshot(`
-        [
-          [
+        Array [
+          Array [
             0,
             0,
           ],
-          [
+          Array [
             70,
             50,
           ],
-          [
+          Array [
             40,
             0,
           ],
@@ -316,12 +315,12 @@ describe("Test Linear Elements", () => {
       expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]);
 
       expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
-        [
-          [
+        Array [
+          Array [
             55.9697848965255,
             47.442326230998205,
           ],
-          [
+          Array [
             76.08587175006699,
             43.294165939653226,
           ],
@@ -364,12 +363,12 @@ describe("Test Linear Elements", () => {
       expect(midPoints[0]).not.toEqual(newMidPoints[0]);
       expect(midPoints[1]).not.toEqual(newMidPoints[1]);
       expect(newMidPoints).toMatchInlineSnapshot(`
-        [
-          [
+        Array [
+          Array [
             105.96978489652551,
             67.4423262309982,
           ],
-          [
+          Array [
             126.08587175006699,
             63.294165939653226,
           ],
@@ -413,29 +412,29 @@ describe("Test Linear Elements", () => {
 
         expect((h.elements[0] as ExcalidrawLinearElement).points)
           .toMatchInlineSnapshot(`
-            [
-              [
-                0,
-                0,
-              ],
-              [
-                85,
-                75,
-              ],
-              [
-                70,
-                50,
-              ],
-              [
-                105,
-                70,
-              ],
-              [
-                40,
-                0,
-              ],
-            ]
-          `);
+          Array [
+            Array [
+              0,
+              0,
+            ],
+            Array [
+              85,
+              75,
+            ],
+            Array [
+              70,
+              50,
+            ],
+            Array [
+              105,
+              70,
+            ],
+            Array [
+              40,
+              0,
+            ],
+          ]
+        `);
       });
 
       it("should update only the first segment midpoint when its point is dragged", async () => {
@@ -559,29 +558,29 @@ describe("Test Linear Elements", () => {
 
         expect((h.elements[0] as ExcalidrawLinearElement).points)
           .toMatchInlineSnapshot(`
-            [
-              [
-                0,
-                0,
-              ],
-              [
-                85.96978489652551,
-                77.4423262309982,
-              ],
-              [
-                70,
-                50,
-              ],
-              [
-                106.08587175006699,
-                73.29416593965323,
-              ],
-              [
-                40,
-                0,
-              ],
-            ]
-          `);
+          Array [
+            Array [
+              0,
+              0,
+            ],
+            Array [
+              85.96978489652551,
+              77.4423262309982,
+            ],
+            Array [
+              70,
+              50,
+            ],
+            Array [
+              106.08587175006699,
+              73.29416593965323,
+            ],
+            Array [
+              40,
+              0,
+            ],
+          ]
+        `);
       });
 
       it("should update all the midpoints when its point is dragged", async () => {
@@ -607,12 +606,12 @@ describe("Test Linear Elements", () => {
         expect(midPoints[0]).not.toEqual(newMidPoints[0]);
         expect(midPoints[1]).not.toEqual(newMidPoints[1]);
         expect(newMidPoints).toMatchInlineSnapshot(`
-          [
-            [
+          Array [
+            Array [
               31.884084517616053,
               23.13275505472383,
             ],
-            [
+            Array [
               77.74792546875662,
               44.57840982272327,
             ],
@@ -668,12 +667,12 @@ describe("Test Linear Elements", () => {
         expect(midPoints[0]).not.toEqual(newMidPoints[0]);
         expect(midPoints[1]).not.toEqual(newMidPoints[1]);
         expect(newMidPoints).toMatchInlineSnapshot(`
-          [
-            [
+          Array [
+            Array [
               55.9697848965255,
               47.442326230998205,
             ],
-            [
+            Array [
               76.08587175006699,
               43.294165939653226,
             ],
@@ -705,12 +704,12 @@ describe("Test Linear Elements", () => {
         [dragEndPositionOffset[0] + line.x, dragEndPositionOffset[1] + line.y],
       );
       expect(line.points).toMatchInlineSnapshot(`
-        [
-          [
+        Array [
+          Array [
             0,
             0,
           ],
-          [
+          Array [
             -60,
             -100,
           ],
@@ -769,7 +768,7 @@ describe("Test Linear Elements", () => {
           textElement,
         );
         expect(position).toMatchInlineSnapshot(`
-          {
+          Object {
             "x": 25,
             "y": 10,
           }
@@ -791,7 +790,7 @@ describe("Test Linear Elements", () => {
           textElement,
         );
         expect(position).toMatchInlineSnapshot(`
-          {
+          Object {
             "x": 75,
             "y": 60,
           }
@@ -825,7 +824,7 @@ describe("Test Linear Elements", () => {
           textElement,
         );
         expect(position).toMatchInlineSnapshot(`
-          {
+          Object {
             "x": 85.82201843191861,
             "y": 75.63461309860818,
           }
@@ -940,11 +939,11 @@ describe("Test Linear Elements", () => {
       expect(textElement.angle).toBe(0);
       expect(getBoundTextElementPosition(arrow, textElement))
         .toMatchInlineSnapshot(`
-          {
-            "x": 75,
-            "y": 60,
-          }
-        `);
+        Object {
+          "x": 75,
+          "y": 60,
+        }
+      `);
       expect(textElement.text).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made 
@@ -952,26 +951,26 @@ describe("Test Linear Elements", () => {
       `);
       expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
         .toMatchInlineSnapshot(`
-          [
-            20,
-            20,
-            105,
-            80,
-            55.45893770831013,
-            45,
-          ]
-        `);
+        Array [
+          20,
+          20,
+          105,
+          80,
+          55.45893770831013,
+          45,
+        ]
+      `);
 
       rotate(container, -35, 55);
       expect(container.angle).toMatchInlineSnapshot(`1.3988061968364685`);
       expect(textElement.angle).toBe(0);
       expect(getBoundTextElementPosition(container, textElement))
         .toMatchInlineSnapshot(`
-          {
-            "x": 21.73926141863671,
-            "y": 73.31003398390868,
-          }
-        `);
+        Object {
+          "x": 21.73926141863671,
+          "y": 73.31003398390868,
+        }
+      `);
       expect(textElement.text).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made 
@@ -979,15 +978,15 @@ describe("Test Linear Elements", () => {
       `);
       expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
         .toMatchInlineSnapshot(`
-          [
-            20,
-            20,
-            102.41961302274555,
-            86.49012635273976,
-            55.45893770831013,
-            45,
-          ]
-        `);
+        Array [
+          20,
+          20,
+          102.41961302274555,
+          86.49012635273976,
+          55.45893770831013,
+          45,
+        ]
+      `);
     });
 
     it("should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized", () => {
@@ -1005,11 +1004,11 @@ describe("Test Linear Elements", () => {
       expect(container.height).toBe(50);
       expect(getBoundTextElementPosition(container, textElement))
         .toMatchInlineSnapshot(`
-          {
-            "x": 75,
-            "y": 60,
-          }
-        `);
+        Object {
+          "x": 75,
+          "y": 60,
+        }
+      `);
       expect(textElement.text).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made 
@@ -1017,33 +1016,33 @@ describe("Test Linear Elements", () => {
       `);
       expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
         .toMatchInlineSnapshot(`
-          [
-            20,
-            20,
-            105,
-            80,
-            55.45893770831013,
-            45,
-          ]
-        `);
+        Array [
+          20,
+          20,
+          105,
+          80,
+          55.45893770831013,
+          45,
+        ]
+      `);
 
       resize(container, "ne", [300, 200]);
 
       expect({ width: container.width, height: container.height })
         .toMatchInlineSnapshot(`
-          {
-            "height": 130,
-            "width": 367,
-          }
-        `);
+        Object {
+          "height": 130,
+          "width": 367,
+        }
+      `);
 
       expect(getBoundTextElementPosition(container, textElement))
         .toMatchInlineSnapshot(`
-          {
-            "x": 272,
-            "y": 45,
-          }
-        `);
+        Object {
+          "x": 272,
+          "y": 45,
+        }
+      `);
       expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
         .toMatchInlineSnapshot(`
         "Online whiteboard 
@@ -1051,15 +1050,15 @@ describe("Test Linear Elements", () => {
       `);
       expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
         .toMatchInlineSnapshot(`
-          [
-            20,
-            35,
-            502,
-            95,
-            205.9061448421403,
-            52.5,
-          ]
-        `);
+        Array [
+          20,
+          35,
+          502,
+          95,
+          205.9061448421403,
+          52.5,
+        ]
+      `);
     });
 
     it("should resize and position the bound text correctly when 2 pointer linear element resized", () => {
@@ -1073,11 +1072,11 @@ describe("Test Linear Elements", () => {
       expect(container.width).toBe(40);
       expect(getBoundTextElementPosition(container, textElement))
         .toMatchInlineSnapshot(`
-          {
-            "x": 25,
-            "y": 10,
-          }
-        `);
+        Object {
+          "x": 25,
+          "y": 10,
+        }
+      `);
       expect(textElement.text).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made 
@@ -1090,19 +1089,19 @@ describe("Test Linear Elements", () => {
 
       expect({ width: container.width, height: container.height })
         .toMatchInlineSnapshot(`
-          {
-            "height": 130,
-            "width": 340,
-          }
-        `);
+        Object {
+          "height": 130,
+          "width": 340,
+        }
+      `);
 
       expect(getBoundTextElementPosition(container, textElement))
         .toMatchInlineSnapshot(`
-          {
-            "x": 75,
-            "y": -5,
-          }
-        `);
+        Object {
+          "x": 75,
+          "y": -5,
+        }
+      `);
       expect(textElement.text).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made easy"
@@ -1155,7 +1154,7 @@ describe("Test Linear Elements", () => {
         "Online whiteboard collaboration
         made easy"
       `);
-      const handleBindTextResizeSpy = vi.spyOn(
+      const handleBindTextResizeSpy = jest.spyOn(
         textElementUtils,
         "handleBindTextResize",
       );

+ 1 - 2
src/tests/move.test.tsx

@@ -12,12 +12,11 @@ import {
 } from "../element/types";
 import { UI, Pointer, Keyboard } from "./helpers/ui";
 import { KEYS } from "../keys";
-import { vi } from "vitest";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = vi.spyOn(Renderer, "renderScene");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

+ 1 - 2
src/tests/multiPointCreate.test.tsx

@@ -10,12 +10,11 @@ import * as Renderer from "../renderer/renderScene";
 import { KEYS } from "../keys";
 import { ExcalidrawLinearElement } from "../element/types";
 import { reseed } from "../random";
-import { vi } from "vitest";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = vi.spyOn(Renderer, "renderScene");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

+ 3 - 3
src/tests/packages/__snapshots__/excalidraw.test.tsx.snap

@@ -1,6 +1,6 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu items if passed from host 1`] = `
+exports[`<Excalidraw/> <MainMenu/> should render main menu with host menu items if passed from host 1`] = `
 <div
   class="dropdown-menu"
   data-testid="dropdown-menu"
@@ -108,7 +108,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
 </div>
 `;
 
-exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should render menu with default items when "UIOPtions" is "undefined" 1`] = `
+exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should render menu with default items when "UIOPtions" is "undefined" 1`] = `
 <div
   class="dropdown-menu"
   data-testid="dropdown-menu"

+ 11 - 11
src/tests/packages/__snapshots__/utils.test.ts.snap

@@ -1,9 +1,9 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+// Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`exportToSvg > with default arguments 1`] = `
-{
+exports[`exportToSvg with default arguments 1`] = `
+Object {
   "activeEmbeddable": null,
-  "activeTool": {
+  "activeTool": Object {
     "customType": null,
     "lastActiveTool": null,
     "locked": false,
@@ -40,7 +40,7 @@ exports[`exportToSvg > with default arguments 1`] = `
   "exportScale": 1,
   "exportWithDarkMode": false,
   "fileHandle": null,
-  "frameRendering": {
+  "frameRendering": Object {
     "clip": true,
     "enabled": true,
     "name": true,
@@ -59,21 +59,21 @@ exports[`exportToSvg > with default arguments 1`] = `
   "openMenu": null,
   "openPopup": null,
   "openSidebar": null,
-  "pasteDialog": {
+  "pasteDialog": Object {
     "data": null,
     "shown": false,
   },
   "penDetected": false,
   "penMode": false,
   "pendingImageElementId": null,
-  "previousSelectedElementIds": {},
+  "previousSelectedElementIds": Object {},
   "resizingElement": null,
   "scrollX": 0,
   "scrollY": 0,
   "scrolledOutside": false,
-  "selectedElementIds": {},
+  "selectedElementIds": Object {},
   "selectedElementsAreBeingDragged": false,
-  "selectedGroupIds": {},
+  "selectedGroupIds": Object {},
   "selectedLinearElement": null,
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
@@ -81,13 +81,13 @@ exports[`exportToSvg > with default arguments 1`] = `
   "showStats": false,
   "showWelcomeScreen": false,
   "startBoundElement": null,
-  "suggestedBindings": [],
+  "suggestedBindings": Array [],
   "theme": "light",
   "toast": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
   "zenModeEnabled": false,
-  "zoom": {
+  "zoom": Object {
     "value": 1,
   },
 }

+ 4 - 6
src/tests/packages/excalidraw.test.tsx

@@ -1,6 +1,6 @@
 import { fireEvent, GlobalTestState, toggleMenu, render } from "../test-utils";
 import { Excalidraw, Footer, MainMenu } from "../../packages/excalidraw/index";
-import { queryByText, queryByTestId, screen } from "@testing-library/react";
+import { queryByText, queryByTestId } from "@testing-library/react";
 import { GRID_SIZE, THEME } from "../../constants";
 import { t } from "../../i18n";
 import { useMemo } from "react";
@@ -42,7 +42,7 @@ describe("<Excalidraw/>", () => {
         container.getElementsByClassName("disable-zen-mode--visible").length,
       ).toBe(0);
       expect(h.state.zenModeEnabled).toBe(true);
-      screen.debug();
+
       fireEvent.contextMenu(GlobalTestState.canvas, {
         button: 2,
         clientX: 1,
@@ -74,8 +74,7 @@ describe("<Excalidraw/>", () => {
         </Footer>
       </Excalidraw>,
     ));
-    expect(container.querySelector(".footer-center")).toMatchInlineSnapshot(
-      `
+    expect(container.querySelector(".footer-center")).toMatchInlineSnapshot(`
       <div
         class="footer-center zen-mode-transition"
       >
@@ -83,8 +82,7 @@ describe("<Excalidraw/>", () => {
           This is a custom footer
         </div>
       </div>
-    `,
-    );
+    `);
   });
 
   describe("Test gridModeEnabled prop", () => {

+ 17 - 13
src/tests/packages/utils.test.ts

@@ -1,13 +1,15 @@
 import * as utils from "../../packages/utils";
 import { diagramFactory } from "../fixtures/diagramFixture";
-import { vi } from "vitest";
 import * as mockedSceneExportUtils from "../../scene/export";
-
 import { MIME_TYPES } from "../../constants";
 
-const exportToSvgSpy = vi.spyOn(mockedSceneExportUtils, "exportToSvg");
+jest.mock("../../scene/export", () => ({
+  __esmodule: true,
+  ...jest.requireActual("../../scene/export"),
+  exportToSvg: jest.fn(),
+}));
 
-describe("exportToCanvas", async () => {
+describe("exportToCanvas", () => {
   const EXPORT_PADDING = 10;
 
   it("with default arguments", async () => {
@@ -30,9 +32,10 @@ describe("exportToCanvas", async () => {
   });
 });
 
-describe("exportToBlob", async () => {
+describe("exportToBlob", () => {
   describe("mime type", () => {
-    // afterEach(vi.restoreAllMocks);
+    afterEach(jest.restoreAllMocks);
+
     it("should change image/jpg to image/jpeg", async () => {
       const blob = await utils.exportToBlob({
         ...diagramFactory(),
@@ -45,6 +48,7 @@ describe("exportToBlob", async () => {
       });
       expect(blob?.type).toBe(MIME_TYPES.jpg);
     });
+
     it("should default to image/png", async () => {
       const blob = await utils.exportToBlob({
         ...diagramFactory(),
@@ -53,14 +57,16 @@ describe("exportToBlob", async () => {
     });
 
     it("should warn when using quality with image/png", async () => {
-      const consoleSpy = vi
+      const consoleSpy = jest
         .spyOn(console, "warn")
         .mockImplementationOnce(() => void 0);
+
       await utils.exportToBlob({
         ...diagramFactory(),
         mimeType: MIME_TYPES.png,
         quality: 1,
       });
+
       expect(consoleSpy).toHaveBeenCalledWith(
         `"quality" will be ignored for "${MIME_TYPES.png}" mimeType`,
       );
@@ -69,12 +75,10 @@ describe("exportToBlob", async () => {
 });
 
 describe("exportToSvg", () => {
-  const passedElements = () => exportToSvgSpy.mock.calls[0][0];
-  const passedOptions = () => exportToSvgSpy.mock.calls[0][1];
-
-  afterEach(() => {
-    vi.clearAllMocks();
-  });
+  const mockedExportUtil = mockedSceneExportUtils.exportToSvg as jest.Mock;
+  const passedElements = () => mockedExportUtil.mock.calls[0][0];
+  const passedOptions = () => mockedExportUtil.mock.calls[0][1];
+  afterEach(jest.resetAllMocks);
 
   it("with default arguments", async () => {
     await utils.exportToSvg({

+ 1 - 3
src/tests/regressionTests.test.tsx

@@ -17,11 +17,10 @@ import {
 } from "./test-utils";
 import { defaultLang } from "../i18n";
 import { FONT_FAMILY } from "../constants";
-import { vi } from "vitest";
 
 const { h } = window;
 
-const renderScene = vi.spyOn(Renderer, "renderScene");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 
 const mouse = new Pointer("mouse");
 const finger1 = new Pointer("touch", 1);
@@ -157,7 +156,6 @@ describe("regression tests", () => {
   }
   it("change the properties of a shape", () => {
     UI.clickTool("rectangle");
-
     mouse.down(10, 10);
     mouse.up(10, 10);
     togglePopover("Background");

+ 1 - 2
src/tests/resize.test.tsx

@@ -9,12 +9,11 @@ import { ExcalidrawTextElement } from "../element/types";
 import ExcalidrawApp from "../excalidraw-app";
 import { API } from "./helpers/api";
 import { KEYS } from "../keys";
-import { vi } from "vitest";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = vi.spyOn(Renderer, "renderScene");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

文件差异内容过多而无法显示
+ 3 - 3
src/tests/scene/__snapshots__/export.test.ts.snap


+ 1 - 1
src/tests/scene/export.test.ts

@@ -61,7 +61,7 @@ describe("exportToSvg", () => {
     );
 
     expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(
-      '"_themeFilter_f32792"',
+      `"themeFilter"`,
     );
   });
 

+ 1 - 2
src/tests/selection.test.tsx

@@ -13,12 +13,11 @@ import { reseed } from "../random";
 import { API } from "./helpers/api";
 import { Keyboard, Pointer, UI } from "./helpers/ui";
 import { SHAPES } from "../shapes";
-import { vi } from "vitest";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = vi.spyOn(Renderer, "renderScene");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

+ 2 - 2
src/utils.ts

@@ -160,7 +160,7 @@ export const throttleRAF = <T extends any[]>(
   };
 
   const ret = (...args: T) => {
-    if (import.meta.env.MODE === "test") {
+    if (process.env.NODE_ENV === "test") {
       fn(...args);
       return;
     }
@@ -772,7 +772,7 @@ export const arrayToMapWithIndex = <T extends { id: string }>(
     return acc;
   }, new Map<string, [element: T, index: number]>());
 
-export const isTestEnv = () => import.meta.env.MODE === "test";
+export const isTestEnv = () => process.env.NODE_ENV === "test";
 
 export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
   return new CustomEvent(name, {

+ 0 - 52
src/vite-env.d.ts

@@ -1,52 +0,0 @@
-/// <reference types="vite/client" />
-/// <reference types="vite-plugin-pwa/react" />
-/// <reference types="vite-plugin-pwa/info" />
-/// <reference types="vite-plugin-svgr/client" />
-
-interface ImportMetaEnv {
-  VITE_APP_BACKEND_V2_GET_URL: string;
-  VITE_APP_BACKEND_V2_POST_URL: string;
-
-  VITE_APP_LIBRARY_URL: string;
-  VITE_APP_LIBRARY_BACKEND: string;
-
-  // collaboration WebSocket server (https: string
-  VITE_APP_WS_SERVER_URL: string;
-
-  // set this only if using the collaboration workflow we use on excalidraw.com
-  VITE_APP_PORTAL_URL: string;
-
-  VITE_APP_FIREBASE_CONFIG: string;
-
-  // whether to enable Service Workers in development
-  VITE_APP_DEV_ENABLE_SW: string;
-  // whether to disable live reload / HMR. Usuaully what you want to do when
-  // debugging Service Workers.
-  VITE_APP_DEV_DISABLE_LIVE_RELOAD: string;
-
-  FAST_REFRESH: string;
-
-  // MATOMO
-  VITE_APP_MATOMO_URL: string;
-  VITE_APP_CDN_MATOMO_TRACKER_URL: string;
-  VITE_APP_MATOMO_SITE_ID: string;
-
-  //Debug flags
-
-  // To enable bounding box for text containers
-  VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX: string;
-  VITE_APP_DISABLE_SENTRY: string;
-
-  VITE_PKG_NAME: string;
-  VITE_PKG_VERSION: string;
-  VITE_IS_EXCALIDRAW_NPM_PACKAGE: string;
-
-  VITE_WORKER_ID: string;
-  MODE: string;
-  DEV: string;
-  PROD: string;
-}
-
-interface ImportMeta {
-  readonly env: ImportMetaEnv;
-}

+ 1 - 2
tsconfig-types.json

@@ -1,7 +1,6 @@
 {
   "include": ["src/packages/excalidraw", "src/global.d.ts", "src/css.d.ts"],
   "compilerOptions": {
-    "types": ["vite/client", "vite-plugin-svgr/client"],
     "allowJs": true,
     "declaration": true,
     "emitDeclarationOnly": true,
@@ -9,7 +8,7 @@
     "jsx": "react-jsx",
     "target": "es6",
     "lib": ["dom", "dom.iterable", "esnext"],
-    "module": "ESNext",
+    "module": "esnext",
     "moduleResolution": "node",
     "resolveJsonModule": true,
     "skipLibCheck": true,

+ 3 - 3
tsconfig.json

@@ -1,6 +1,6 @@
 {
   "compilerOptions": {
-    "target": "ESNext",
+    "target": "es6",
     "lib": ["dom", "dom.iterable", "esnext"],
     "allowJs": true,
     "skipLibCheck": true,
@@ -8,9 +8,9 @@
     "allowSyntheticDefaultImports": true,
     "strict": true,
     "forceConsistentCasingInFileNames": true,
-    "noFallthroughCasesInSwitch": true,
-    "module": "ESNext",
+    "module": "esnext",
     "moduleResolution": "node",
+    "noFallthroughCasesInSwitch": true,
     "resolveJsonModule": true,
     "isolatedModules": true,
     "noEmit": true,

+ 0 - 163
vite.config.ts

@@ -1,163 +0,0 @@
-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react";
-import svgrPlugin from "vite-plugin-svgr";
-import { ViteEjsPlugin } from "vite-plugin-ejs";
-import { VitePWA } from "vite-plugin-pwa";
-import eslint from "vite-plugin-eslint";
-
-// https://vitejs.dev/config/
-export default defineConfig({
-  build: {
-    outDir: "build",
-    rollupOptions: {
-      output: {
-        // Creating separate chunk for locales except for en and percentages.json so they
-        // can be cached at runtime and not merged with
-        // app precache. en.json and percentages.json are needed for first load
-        // or fallback hence not clubbing with locales so first load followed by offline mode works fine. This is how CRA used to work too.
-        manualChunks(id) {
-          if (
-            id.includes("src/locales") &&
-            id.match(/en.json|percentages.json/) === null
-          ) {
-            const index = id.indexOf("locales/");
-            // Taking the substring after "locales/"
-            return `locales/${id.substring(index + 8)}`;
-          }
-        },
-      },
-    },
-    sourcemap: true,
-  },
-  plugins: [
-    react(),
-    eslint(),
-    svgrPlugin(),
-    ViteEjsPlugin(),
-    VitePWA({
-      devOptions: {
-        /* set this flag to true to enable in Development mode */
-        enabled: false,
-      },
-
-      workbox: {
-        // Don't push fonts and locales to app precache
-        globIgnores: ["fonts.css", "**/locales/**"],
-        runtimeCaching: [
-          {
-            urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),
-            handler: "CacheFirst",
-            options: {
-              cacheName: "fonts",
-              expiration: {
-                maxEntries: 50,
-                maxAgeSeconds: 60 * 60 * 24 * 90, // <== 90 days
-              },
-            },
-          },
-          {
-            urlPattern: new RegExp("fonts.css"),
-            handler: "StaleWhileRevalidate",
-            options: {
-              cacheName: "fonts",
-              expiration: {
-                maxEntries: 50,
-              },
-            },
-          },
-          {
-            urlPattern: new RegExp("locales/[^/]+.js"),
-            handler: "CacheFirst",
-            options: {
-              cacheName: "locales",
-              expiration: {
-                maxEntries: 50,
-                maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days
-              },
-            },
-          },
-        ],
-      },
-      manifest: {
-        short_name: "Excalidraw",
-        name: "Excalidraw",
-        description:
-          "Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them.",
-        icons: [
-          {
-            src: "logo-180x180.png",
-            sizes: "180x180",
-            type: "image/png",
-          },
-          {
-            src: "apple-touch-icon.png",
-            type: "image/png",
-            sizes: "256x256",
-          },
-        ],
-        start_url: "/",
-        display: "standalone",
-        theme_color: "#121212",
-        background_color: "#ffffff",
-        file_handlers: [
-          {
-            action: "/",
-            accept: {
-              "application/vnd.excalidraw+json": [".excalidraw"],
-            },
-          },
-        ],
-        share_target: {
-          action: "/web-share-target",
-          method: "POST",
-          enctype: "multipart/form-data",
-          params: {
-            files: [
-              {
-                name: "file",
-                accept: [
-                  "application/vnd.excalidraw+json",
-                  "application/json",
-                  ".excalidraw",
-                ],
-              },
-            ],
-          },
-        },
-        screenshots: [
-          {
-            src: "/screenshots/virtual-whiteboard.png",
-            type: "image/png",
-            sizes: "462x945",
-          },
-          {
-            src: "/screenshots/wireframe.png",
-            type: "image/png",
-            sizes: "462x945",
-          },
-          {
-            src: "/screenshots/illustration.png",
-            type: "image/png",
-            sizes: "462x945",
-          },
-          {
-            src: "/screenshots/shapes.png",
-            type: "image/png",
-            sizes: "462x945",
-          },
-          {
-            src: "/screenshots/collaboration.png",
-            type: "image/png",
-            sizes: "462x945",
-          },
-          {
-            src: "/screenshots/export.png",
-            type: "image/png",
-            sizes: "462x945",
-          },
-        ],
-      },
-    }),
-  ],
-  publicDir: "./public",
-});

+ 0 - 9
vitest.config.ts

@@ -1,9 +0,0 @@
-import { defineConfig } from "vitest/config";
-
-export default defineConfig({
-  test: {
-    setupFiles: ["./src/setupTests.ts"],
-    globals: true,
-    environment: "jsdom",
-  },
-});

文件差异内容过多而无法显示
+ 331 - 437
yarn.lock


部分文件因为文件数量过多而无法显示