From 7e5bac379c597a7cdbd0f7f55d7e5a9737f08a3a Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Sat, 28 Feb 2026 11:09:32 -0500 Subject: [PATCH] chore: switch from bun test to vitest for test isolation support --- .github/workflows/ci.yml | 2 +- CLAUDE.md | 24 +-- README.md | 4 +- bun.lock | 179 ++++++++++++++++++ package.json | 9 +- src/commands/frames.command.test.ts | 12 +- src/commands/mem.command.test.ts | 37 ++-- src/handlers/message.handler.test.ts | 34 ++-- .../achievement-unlocks.service.test.ts | 40 ++-- src/services/achievement-unlocks.service.ts | 4 +- src/services/auto-publish.service.test.ts | 44 +++-- src/services/connect-api.service.test.ts | 4 +- src/services/dadjoke.service.test.ts | 49 ++--- src/services/frames.service.test.ts | 2 +- src/services/game-info.service.test.ts | 30 +-- src/services/github-release.service.test.ts | 47 +++-- src/services/poll.service.test.ts | 25 ++- src/services/team.service.test.ts | 29 ++- src/services/template.service.test.ts | 2 +- src/services/uwc-poll.service.test.ts | 82 ++++---- src/services/youtube.service.test.ts | 62 +++--- src/slash-commands/frames.command.test.ts | 26 +-- src/slash-commands/pingteam.command.test.ts | 108 +++++------ src/slash-commands/uwc.command.test.ts | 92 ++++----- src/test/mocks/database.mock.ts | 118 ------------ src/test/mocks/discord.mock.ts | 46 ++--- src/utils/admin-checker.test.ts | 2 +- src/utils/command-analytics.test.ts | 4 +- src/utils/cooldown-manager.test.ts | 2 +- src/utils/error-tracker.test.ts | 4 +- src/utils/guild-manager.test.ts | 18 +- src/utils/guild-restrictions.test.ts | 32 ++-- src/utils/memory-parser.test.ts | 2 +- vitest.config.ts | 7 + 34 files changed, 625 insertions(+), 557 deletions(-) create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67fa253..bff1c4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,4 +26,4 @@ jobs: bun run db:generate bun run db:migrate - - run: bun test + - run: bun run test diff --git a/CLAUDE.md b/CLAUDE.md index c57f542..f9c0c3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,8 +17,8 @@ bun run dev # Run with hot reload (--watch) bun run tsc # TypeScript type checking bun run lint # Run ESLint bun run lint:fix # Auto-fix linting issues -bun test # Run all tests -bun test:watch # Run tests in watch mode +bun run test # Run all tests (vitest) +bun run test:watch # Run tests in watch mode (vitest) bun run verify # Run lint, type checking, and tests (comprehensive check) # Deployment @@ -234,25 +234,7 @@ The bot uses Pino for structured logging with the following features: ## Testing -### CI Environment Compatibility - -Some tests may need to be conditionally skipped in CI environments due to infrastructure differences (e.g., Drizzle ORM compatibility issues with GitHub Actions). Use this pattern for database-dependent tests: - -```typescript -// Skip database-dependent tests in CI environment where Drizzle methods may be undefined. -const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; -const describeOrSkip = isCI ? describe.skip : describe; - -describeOrSkip("DatabaseDependentService", () => { - // Tests that require database functionality -}); -``` - -This ensures: - -- Tests run normally in local development -- CI builds pass by skipping problematic tests -- Easy to remove when underlying issues are resolved +Tests use [Vitest](https://vitest.dev/) as the test runner (configured in `vitest.config.ts`). All tests run in both local development and CI environments. ## Common Gotchas diff --git a/README.md b/README.md index 4c2b140..55f19f6 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,8 @@ For production deployments, the bot is automatically deployed via Forge when cha - `bun lint` - Run ESLint - `bun lint:fix` - Run ESLint with auto-fix - `bun tsc` - Run TypeScript type checking -- `bun test` - Run all tests -- `bun test:watch` - Run tests in watch mode +- `bun run test` - Run all tests +- `bun run test:watch` - Run tests in watch mode - `bun verify` - Run lint, type checking, and tests (comprehensive check) ## Commands diff --git a/bun.lock b/bun.lock index 10f52de..07a744b 100644 --- a/bun.lock +++ b/bun.lock @@ -25,6 +25,7 @@ "eslint-plugin-sort-keys-shorthand": "^3.0.0", "eslint-plugin-unicorn": "^59.0.1", "typescript-eslint": "^8.37.0", + "vitest": "^4.0.18", }, "peerDependencies": { "typescript": "^5", @@ -136,6 +137,8 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@libsql/client": ["@libsql/client@0.17.0", "", { "dependencies": { "@libsql/core": "^0.17.0", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg=="], "@libsql/core": ["@libsql/core@0.17.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-hnZRnJHiS+nrhHKLGYPoJbc78FE903MSDrFJTbftxo+e52X+E0Y0fHOCVYsKWcg6XgB7BbJYUrz/xEkVTSaipw=="], @@ -176,6 +179,56 @@ "@retroachievements/api": ["@retroachievements/api@2.6.0", "", {}, "sha512-ra6tSHYRJ1Mdm25GtlwQUAQLfrad32hOfBwYRHQo+Fv+vjeki3jEtXX/KNqFGZK+5DKH6S1rSgVoGBdXVbNzkA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], @@ -184,12 +237,18 @@ "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/figlet": ["@types/figlet@1.7.0", "", {}, "sha512-KwrT7p/8Eo3Op/HBSIwGXOsTZKYiM9NpWRBJ5sVjWP/SmlS+oxxRvJht/FNAtliJvja44N3ul1yATgohnVBV0Q=="], @@ -262,6 +321,20 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], + + "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], + + "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + + "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -286,6 +359,8 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], @@ -326,6 +401,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], @@ -392,6 +469,8 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -442,10 +521,14 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], @@ -486,6 +569,8 @@ "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], @@ -630,6 +715,8 @@ "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], @@ -648,6 +735,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], "napi-postinstall": ["napi-postinstall@0.3.0", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA=="], @@ -674,6 +763,8 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -692,6 +783,8 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -700,6 +793,8 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -736,6 +831,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], @@ -768,16 +865,24 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], "stable-hash-x": ["stable-hash-x@0.2.0", "", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], @@ -804,8 +909,14 @@ "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], @@ -848,6 +959,10 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -864,6 +979,8 @@ "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -922,6 +1039,14 @@ "regjsparser/jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], + "vite/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "vite/fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "vite/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "vitest/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -969,5 +1094,59 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "eslint-plugin-unicorn/@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "vitest/tinyglobby/fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], } } diff --git a/package.json b/package.json index f5bfe27..d32a041 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,9 @@ "tsc": "tsc --noEmit", "lint": "eslint src --cache", "lint:fix": "eslint src --fix", - "test": "bun test", - "test:watch": "bun test --watch", - "verify": "bun run lint:fix && bun run tsc && bun test", + "test": "vitest run", + "test:watch": "vitest", + "verify": "bun run lint:fix && bun run tsc && vitest run", "postinstall": "git config core.hooksPath .hooks && chmod +x .hooks/*" }, "dependencies": { @@ -57,7 +57,8 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-sort-keys-shorthand": "^3.0.0", "eslint-plugin-unicorn": "^59.0.1", - "typescript-eslint": "^8.37.0" + "typescript-eslint": "^8.37.0", + "vitest": "^4.0.18" }, "peerDependencies": { "typescript": "^5" diff --git a/src/commands/frames.command.test.ts b/src/commands/frames.command.test.ts index b082c4f..c70736e 100644 --- a/src/commands/frames.command.test.ts +++ b/src/commands/frames.command.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { FramesService } from "../services/frames.service"; import { createMockMessage } from "../test/mocks/discord.mock"; @@ -12,7 +12,7 @@ describe("Command: frames", () => { }); afterEach(() => { - mock.restore(); + vi.restoreAllMocks(); }); it("is defined", () => { @@ -34,7 +34,7 @@ describe("Command: frames", () => { it("shows error message for invalid input", async () => { // ARRANGE - spyOn(FramesService, "processInput").mockReturnValue(null); + vi.spyOn(FramesService, "processInput").mockReturnValue(null); // ACT await framesCommand.execute(mockMessage, ["invalid", "input"], {} as any); @@ -49,7 +49,7 @@ describe("Command: frames", () => { // ARRANGE const expectedOutput = "**Time:** `1h 0min 0s 0ms`\n**FPS:** `60`\n**Frames:** `216000 (0x34bc0)`"; - spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); + vi.spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); // ACT await framesCommand.execute(mockMessage, ["1h"], {} as any); @@ -63,7 +63,7 @@ describe("Command: frames", () => { // ARRANGE const expectedOutput = "**Time:** `0h 1min 0s 0ms`\n**FPS:** `60`\n**Frames:** `3600 (0xe10)`"; - spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); + vi.spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); // ACT await framesCommand.execute(mockMessage, ["3600"], {} as any); @@ -77,7 +77,7 @@ describe("Command: frames", () => { // ARRANGE const expectedOutput = "**Time:** `1h 30min 0s 0ms`\n**FPS:** `30`\n**Frames:** `162000 (0x27990)`"; - spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); + vi.spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); // ACT await framesCommand.execute(mockMessage, ["1h", "30min", "30fps"], {} as any); diff --git a/src/commands/mem.command.test.ts b/src/commands/mem.command.test.ts index 5bc8349..12eb6ec 100644 --- a/src/commands/mem.command.test.ts +++ b/src/commands/mem.command.test.ts @@ -1,26 +1,31 @@ -import * as ra from "@retroachievements/api"; -import { afterEach, beforeEach, describe, expect, it, type Mock, mock, spyOn } from "bun:test"; import type { Message } from "discord.js"; +import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from "vitest"; import { connectApiService } from "../services/connect-api.service"; import { createMockMessage } from "../test/mocks/discord.mock"; import memCommand from "./mem.command"; +const { mockGetAchievementUnlocks, mockBuildAuthorization } = vi.hoisted(() => ({ + mockGetAchievementUnlocks: vi.fn(), + mockBuildAuthorization: vi.fn(() => ({ username: "RABot", webApiKey: "test" })), +})); + +vi.mock("@retroachievements/api", () => ({ + getAchievementUnlocks: mockGetAchievementUnlocks, + buildAuthorization: mockBuildAuthorization, +})); + describe("Command: mem", () => { let mockMessage: ReturnType; - const mockGetAchievementUnlocks = mock(); beforeEach(() => { mockMessage = createMockMessage(); mockGetAchievementUnlocks.mockReset(); - - // ... mock the @retroachievements/api functions ... - spyOn(ra, "getAchievementUnlocks").mockImplementation(mockGetAchievementUnlocks); - spyOn(ra, "buildAuthorization").mockReturnValue({ username: "RABot", webApiKey: "test" }); + mockBuildAuthorization.mockClear(); }); afterEach(() => { - mock.restore(); + vi.restoreAllMocks(); }); it("is defined", () => { @@ -69,14 +74,14 @@ describe("Command: mem", () => { it("processes achievement ID successfully", async () => { // ARRANGE - const sentMsg = { edit: mock() } as unknown as Message; + const sentMsg = { edit: vi.fn() } as unknown as Message; (mockMessage.reply as Mock<() => Promise>).mockResolvedValueOnce(sentMsg); mockGetAchievementUnlocks.mockResolvedValueOnce({ game: { id: 789 }, }); - spyOn(connectApiService, "getMemAddr").mockResolvedValueOnce("0xH1234=5"); + vi.spyOn(connectApiService, "getMemAddr").mockResolvedValueOnce("0xH1234=5"); // ACT await memCommand.execute(mockMessage, ["123456"], {} as any); @@ -90,14 +95,14 @@ describe("Command: mem", () => { it("processes achievement URL successfully", async () => { // ARRANGE - const sentMsg = { edit: mock() } as unknown as Message; + const sentMsg = { edit: vi.fn() } as unknown as Message; (mockMessage.reply as Mock<() => Promise>).mockResolvedValueOnce(sentMsg); mockGetAchievementUnlocks.mockResolvedValueOnce({ game: { id: 789 }, }); - spyOn(connectApiService, "getMemAddr").mockResolvedValueOnce("0xH1234=5"); + vi.spyOn(connectApiService, "getMemAddr").mockResolvedValueOnce("0xH1234=5"); // ACT await memCommand.execute( @@ -115,7 +120,7 @@ describe("Command: mem", () => { it("shows error message when game ID not found", async () => { // ARRANGE - const sentMsg = { edit: mock() } as unknown as Message; + const sentMsg = { edit: vi.fn() } as unknown as Message; (mockMessage.reply as Mock<() => Promise>).mockResolvedValueOnce(sentMsg); mockGetAchievementUnlocks.mockResolvedValueOnce({}); @@ -131,14 +136,14 @@ describe("Command: mem", () => { it("shows error message when MemAddr not found", async () => { // ARRANGE - const sentMsg = { edit: mock() } as unknown as Message; + const sentMsg = { edit: vi.fn() } as unknown as Message; (mockMessage.reply as Mock<() => Promise>).mockResolvedValueOnce(sentMsg); mockGetAchievementUnlocks.mockResolvedValueOnce({ game: { id: 789 }, }); - spyOn(connectApiService, "getMemAddr").mockResolvedValueOnce(null); + vi.spyOn(connectApiService, "getMemAddr").mockResolvedValueOnce(null); // ACT await memCommand.execute(mockMessage, ["123456"], {} as any); @@ -151,7 +156,7 @@ describe("Command: mem", () => { it("handles API errors gracefully", async () => { // ARRANGE - const sentMsg = { edit: mock() } as unknown as Message; + const sentMsg = { edit: vi.fn() } as unknown as Message; (mockMessage.reply as Mock<() => Promise>).mockResolvedValueOnce(sentMsg); mockGetAchievementUnlocks.mockRejectedValueOnce(new Error("API Error")); diff --git a/src/handlers/message.handler.test.ts b/src/handlers/message.handler.test.ts index bec704b..c412496 100644 --- a/src/handlers/message.handler.test.ts +++ b/src/handlers/message.handler.test.ts @@ -1,5 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; import { Collection, PermissionsBitField } from "discord.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { BotClient, Command, SlashCommand } from "../models"; import { createMockClient, createMockMessage } from "../test/mocks/discord.mock"; @@ -16,7 +16,7 @@ describe("Handler: handleMessage", () => { afterEach(() => { // ... clear all mock calls ... - mock.restore(); + vi.restoreAllMocks(); }); beforeEach(() => { @@ -27,7 +27,7 @@ describe("Handler: handleMessage", () => { description: "Test command", usage: "!test", category: "utility", - execute: mock(() => Promise.resolve()), + execute: vi.fn(() => Promise.resolve()), }; mockSlashCommand = { @@ -36,7 +36,7 @@ describe("Handler: handleMessage", () => { description: "Test slash command", } as any, legacyName: "test", - execute: mock(() => Promise.resolve()), + execute: vi.fn(() => Promise.resolve()), }; mockClient = createMockClient({ @@ -46,15 +46,17 @@ describe("Handler: handleMessage", () => { }); // ... spy on utility functions ... - spyOn(CooldownManager, "checkCooldown").mockReturnValue(0); - spyOn(CooldownManager, "setCooldown").mockImplementation(() => {}); - spyOn(CooldownManager, "formatCooldownMessage").mockReturnValue("⏱️ Please wait **3** seconds"); - spyOn(CommandAnalytics, "startTracking").mockReturnValue(Date.now()); - spyOn(CommandAnalytics, "trackLegacyCommand").mockImplementation(() => {}); - spyOn(logger, "logCommandExecution").mockImplementation(() => logger.logger); - spyOn(logger, "logError").mockImplementation(() => {}); - spyOn(logger, "logMigrationNotice").mockImplementation(() => {}); - spyOn(migrationHelper, "sendMigrationNotice").mockResolvedValue(null); + vi.spyOn(CooldownManager, "checkCooldown").mockReturnValue(0); + vi.spyOn(CooldownManager, "setCooldown").mockImplementation(() => {}); + vi.spyOn(CooldownManager, "formatCooldownMessage").mockReturnValue( + "⏱️ Please wait **3** seconds", + ); + vi.spyOn(CommandAnalytics, "startTracking").mockReturnValue(Date.now()); + vi.spyOn(CommandAnalytics, "trackLegacyCommand").mockImplementation(() => {}); + vi.spyOn(logger, "logCommandExecution").mockImplementation(() => logger.logger); + vi.spyOn(logger, "logError").mockImplementation(() => {}); + vi.spyOn(logger, "logMigrationNotice").mockImplementation(() => {}); + vi.spyOn(migrationHelper, "sendMigrationNotice").mockResolvedValue(null); }); it("is defined", () => { @@ -153,8 +155,8 @@ describe("Handler: handleMessage", () => { }); (CooldownManager.checkCooldown as any).mockReturnValue(3000); // ... 3 seconds remaining ... - const mockReply = { delete: mock(() => Promise.resolve()) }; - message.reply = mock(() => Promise.resolve(mockReply as any)); + const mockReply = { delete: vi.fn(() => Promise.resolve()) }; + message.reply = vi.fn(() => Promise.resolve(mockReply as any)); // ACT await handleMessage(message, mockClient); @@ -283,7 +285,7 @@ describe("Handler: handleMessage", () => { it("handles and logs command execution errors", async () => { // ARRANGE const testError = new Error("Test error"); - mockCommand.execute = mock(() => Promise.reject(testError)); + mockCommand.execute = vi.fn(() => Promise.reject(testError)); const message = createMockMessage({ content: "!test", diff --git a/src/services/achievement-unlocks.service.test.ts b/src/services/achievement-unlocks.service.test.ts index e41fb2f..eeefaed 100644 --- a/src/services/achievement-unlocks.service.test.ts +++ b/src/services/achievement-unlocks.service.test.ts @@ -1,31 +1,33 @@ -import { beforeEach, describe, expect, it, mock, test } from "bun:test"; +import { beforeEach, describe, expect, it, test, vi } from "vitest"; import { createMockAchievementUnlocks } from "../test/mocks/achievement-unlocks.mock"; import { AchievementUnlocksService, PAGE_SIZE } from "./achievement-unlocks.service"; -// ... mock the @retroachievements/api module ... -const mockBuildAuthorization = mock(() => ({ username: "RABot", webApiKey: "test-key" })); -const mockGetAchievementUnlocks = mock(async (_auth, { achievementId, offset, count }) => { - if (achievementId === 99999) { - throw new Error("API Error: 404"); - } else { - const data = createMockAchievementUnlocks(achievementId); - data.unlocks = data.unlocks.slice(offset, offset + count); - - return data; - } -}); +const { mockBuildAuthorization, mockGetAchievementUnlocks } = vi.hoisted(() => ({ + mockBuildAuthorization: vi.fn(() => ({ username: "RABot", webApiKey: "test-key" })), + mockGetAchievementUnlocks: vi.fn(), +})); -mock.module("@retroachievements/api", () => ({ +vi.mock("@retroachievements/api", () => ({ buildAuthorization: mockBuildAuthorization, getAchievementUnlocks: mockGetAchievementUnlocks, })); describe("Service: AchievementUnlocksService", () => { beforeEach(() => { - // ... reset mocks ... mockBuildAuthorization.mockClear(); - mockGetAchievementUnlocks.mockClear(); + mockGetAchievementUnlocks + .mockClear() + .mockImplementation(async (_auth: any, { achievementId, offset, count }: any) => { + if (achievementId === 99999) { + throw new Error("API Error: 404"); + } + + const data = createMockAchievementUnlocks(achievementId); + data.unlocks = data.unlocks.slice(offset, offset + count); + + return data; + }); }); describe("getAllAchievementUnlocks", () => { @@ -41,7 +43,7 @@ describe("Service: AchievementUnlocksService", () => { const result = await AchievementUnlocksService.getAllAchievementUnlocks(n); // ASSERT - expect(result).toBeArrayOfSize(n); + expect(result).toHaveLength(n); expect(result!.at(0)).toBe("User0"); expect(result!.at(-1)).toBe(`User${n - 1}`); expect(mockGetAchievementUnlocks).toHaveBeenCalledTimes(Math.ceil(n / PAGE_SIZE)); @@ -59,7 +61,7 @@ describe("Service: AchievementUnlocksService", () => { const result = await AchievementUnlocksService.getAllAchievementUnlocks(0); // ASSERT - expect(result).toBeArrayOfSize(0); + expect(result).toHaveLength(0); }); it("returns null if the achievement is not found", async () => { @@ -78,7 +80,7 @@ describe("Service: AchievementUnlocksService", () => { const result = await AchievementUnlocksService.getAllAchievementUnlocks(1); // ASSERT - expect(result).toBeArrayOfSize(1); + expect(result).toHaveLength(1); expect(mockGetAchievementUnlocks).toHaveBeenCalledTimes(2); }); diff --git a/src/services/achievement-unlocks.service.ts b/src/services/achievement-unlocks.service.ts index 75d09ff..a4ef94d 100644 --- a/src/services/achievement-unlocks.service.ts +++ b/src/services/achievement-unlocks.service.ts @@ -19,7 +19,9 @@ export class AchievementUnlocksService { }); } catch (error) { if (error instanceof Error && error.message.includes("429")) { - await new Promise((resolve) => setTimeout(() => resolve(), Math.pow(2, tries) * 200)); + await new Promise((resolve) => + setTimeout(() => resolve(), Math.pow(2, tries) * 200), + ); continue; } else { return null; diff --git a/src/services/auto-publish.service.test.ts b/src/services/auto-publish.service.test.ts index 9eeae20..48357ae 100644 --- a/src/services/auto-publish.service.test.ts +++ b/src/services/auto-publish.service.test.ts @@ -1,23 +1,23 @@ -import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; import { ChannelType, MessageFlags } from "discord.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { AutoPublishService } from "./auto-publish.service"; // Mock the constants module. -mock.module("../config/constants", () => ({ +vi.mock("../config/constants", () => ({ AUTO_PUBLISH_CHANNEL_IDS: ["123456789012345678", "987654321098765432"], })); // Mock the logger module. -mock.module("../utils/logger", () => ({ +vi.mock("../utils/logger", () => ({ logger: { - info: mock(), - warn: mock(), - error: mock(), - debug: mock(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), }, - logError: mock(), - logApiCall: mock(), + logError: vi.fn(), + logApiCall: vi.fn(), })); describe("Service: AutoPublishService", () => { @@ -27,9 +27,9 @@ describe("Service: AutoPublishService", () => { channelId: "123456789012345678", author: { bot: false, tag: "TestUser#1234" }, flags: { - has: mock((_flag: MessageFlags) => false), + has: vi.fn((_flag: MessageFlags) => false), }, - crosspost: mock(), + crosspost: vi.fn(), id: "message123", guildId: "guild123", ...overrides, @@ -37,7 +37,7 @@ describe("Service: AutoPublishService", () => { beforeEach(() => { // Clear all mocks before each test. - mock.restore(); + vi.restoreAllMocks(); }); describe("shouldAutoPublish", () => { @@ -76,7 +76,7 @@ describe("Service: AutoPublishService", () => { // ARRANGE const message = createMockMessage({ flags: { - has: mock((flag: MessageFlags) => flag === MessageFlags.Crossposted), + has: vi.fn((flag: MessageFlags) => flag === MessageFlags.Crossposted), }, }); @@ -185,10 +185,10 @@ describe("Service: AutoPublishService", () => { const message = createMockMessage({ channel: { type: ChannelType.GuildText }, }); - const shouldAutoPublishSpy = spyOn(AutoPublishService, "shouldAutoPublish").mockReturnValue( - false, - ); - const publishMessageSpy = spyOn(AutoPublishService, "publishMessage"); + const shouldAutoPublishSpy = vi + .spyOn(AutoPublishService, "shouldAutoPublish") + .mockReturnValue(false); + const publishMessageSpy = vi.spyOn(AutoPublishService, "publishMessage"); // ACT await AutoPublishService.handleMessage(message as any); @@ -201,10 +201,12 @@ describe("Service: AutoPublishService", () => { it("attempts to publish if message should be auto-published", async () => { // ARRANGE const message = createMockMessage(); - const shouldAutoPublishSpy = spyOn(AutoPublishService, "shouldAutoPublish").mockReturnValue( - true, - ); - const publishMessageSpy = spyOn(AutoPublishService, "publishMessage").mockResolvedValue(true); + const shouldAutoPublishSpy = vi + .spyOn(AutoPublishService, "shouldAutoPublish") + .mockReturnValue(true); + const publishMessageSpy = vi + .spyOn(AutoPublishService, "publishMessage") + .mockResolvedValue(true); // ACT await AutoPublishService.handleMessage(message as any); diff --git a/src/services/connect-api.service.test.ts b/src/services/connect-api.service.test.ts index 6637a7d..0d41193 100644 --- a/src/services/connect-api.service.test.ts +++ b/src/services/connect-api.service.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { connectApiService } from "./connect-api.service"; describe("Service: connect-api", () => { const originalFetch = global.fetch; - const mockFetch = mock(); + const mockFetch = vi.fn(); beforeEach(() => { // @ts-expect-error - global.fetch is assignable diff --git a/src/services/dadjoke.service.test.ts b/src/services/dadjoke.service.test.ts index 81d5888..5206f73 100644 --- a/src/services/dadjoke.service.test.ts +++ b/src/services/dadjoke.service.test.ts @@ -1,30 +1,35 @@ -import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as logger from "../utils/logger"; import { DadjokeService } from "./dadjoke.service"; describe("DadjokeService", () => { + const originalFetch = global.fetch; + beforeEach(() => { - // Reset mocks. - spyOn(logger, "logApiCall").mockImplementation(() => {}); - spyOn(logger, "logError").mockImplementation(() => {}); + vi.spyOn(logger, "logApiCall").mockImplementation(() => {}); + vi.spyOn(logger, "logError").mockImplementation(() => {}); + }); + + afterEach(() => { + global.fetch = originalFetch; }); describe("fetchRandomJoke", () => { it("should return a joke when API call is successful", async () => { - // Given + // ARRANGE const mockJoke = "Why don't scientists trust atoms? Because they make up everything!"; const mockResponse = { ok: true, status: 200, - json: mock(() => Promise.resolve({ joke: mockJoke })), + json: vi.fn(() => Promise.resolve({ joke: mockJoke })), }; - global.fetch = mock(() => Promise.resolve(mockResponse)) as any; + global.fetch = vi.fn(() => Promise.resolve(mockResponse)) as any; - // When + // ACT const result = await DadjokeService.fetchRandomJoke(); - // Then + // ASSERT expect(result).toBe(mockJoke); expect(fetch).toHaveBeenCalledWith("https://icanhazdadjoke.com/", { headers: { @@ -35,45 +40,45 @@ describe("DadjokeService", () => { }); it("should return null when API response is not ok", async () => { - // Given + // ARRANGE const mockResponse = { ok: false, status: 500, statusText: "Internal Server Error", }; - global.fetch = mock(() => Promise.resolve(mockResponse)) as any; + global.fetch = vi.fn(() => Promise.resolve(mockResponse)) as any; - // When + // ACT const result = await DadjokeService.fetchRandomJoke(); - // Then + // ASSERT expect(result).toBeNull(); }); it("should return null when fetch throws an error", async () => { - // Given - global.fetch = mock(() => Promise.reject(new Error("Network error"))) as any; + // ARRANGE + global.fetch = vi.fn(() => Promise.reject(new Error("Network error"))) as any; - // When + // ACT const result = await DadjokeService.fetchRandomJoke(); - // Then + // ASSERT expect(result).toBeNull(); }); it("should return null when API response has no joke property", async () => { - // Given + // ARRANGE const mockResponse = { ok: true, status: 200, - json: mock(() => Promise.resolve({})), + json: vi.fn(() => Promise.resolve({})), }; - global.fetch = mock(() => Promise.resolve(mockResponse)) as any; + global.fetch = vi.fn(() => Promise.resolve(mockResponse)) as any; - // When + // ACT const result = await DadjokeService.fetchRandomJoke(); - // Then + // ASSERT expect(result).toBeNull(); }); }); diff --git a/src/services/frames.service.test.ts b/src/services/frames.service.test.ts index 666d892..0348cfb 100644 --- a/src/services/frames.service.test.ts +++ b/src/services/frames.service.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "bun:test"; +import { describe, expect, it } from "vitest"; import { FramesService } from "./frames.service"; diff --git a/src/services/game-info.service.test.ts b/src/services/game-info.service.test.ts index 98bdc56..ab428b8 100644 --- a/src/services/game-info.service.test.ts +++ b/src/services/game-info.service.test.ts @@ -1,30 +1,30 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createMockGameExtended } from "../test/mocks/game-data.mock"; import { GameInfoService } from "./game-info.service"; -// ... mock the @retroachievements/api module ... -const mockBuildAuthorization = mock(() => ({ username: "RABot", webApiKey: "test-key" })); -const mockGetGameExtended = mock(async (auth, { gameId }) => { - if (gameId === 14402) { - return createMockGameExtended(); - } - if (gameId === 99999) { - return null; - } - throw new Error("API Error"); -}); +const { mockBuildAuthorization, mockGetGameExtended } = vi.hoisted(() => ({ + mockBuildAuthorization: vi.fn(() => ({ username: "RABot", webApiKey: "test-key" })), + mockGetGameExtended: vi.fn(), +})); -mock.module("@retroachievements/api", () => ({ +vi.mock("@retroachievements/api", () => ({ buildAuthorization: mockBuildAuthorization, getGameExtended: mockGetGameExtended, })); describe("Service: GameInfoService", () => { beforeEach(() => { - // ... reset mocks ... mockBuildAuthorization.mockClear(); - mockGetGameExtended.mockClear(); + mockGetGameExtended.mockClear().mockImplementation(async (_auth: any, { gameId }: any) => { + if (gameId === 14402) { + return createMockGameExtended(); + } + if (gameId === 99999) { + return null; + } + throw new Error("API Error"); + }); }); describe("extractGameId", () => { diff --git a/src/services/github-release.service.test.ts b/src/services/github-release.service.test.ts index 8b36434..4500aa6 100644 --- a/src/services/github-release.service.test.ts +++ b/src/services/github-release.service.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { GithubReleaseService } from "./github-release.service"; @@ -17,18 +17,21 @@ describe("GithubReleaseService", () => { describe("fetchLatestVersion", () => { it("should fetch and return the version tag from GitHub API", async () => { - const mockFetch = mock(() => + // ARRANGE + const mockFetch = vi.fn(() => Promise.resolve({ ok: true, status: 200, - json: mock(() => Promise.resolve({ tag_name: "2.0.1" })), + json: vi.fn(() => Promise.resolve({ tag_name: "2.0.1" })), }), ); // @ts-expect-error - global.fetch is assignable global.fetch = mockFetch; + // ACT const version = await GithubReleaseService.fetchLatestVersion(); + // ASSERT expect(version).toBe("2.0.1"); expect(mockFetch).toHaveBeenCalledWith( "https://api.github.com/repos/RetroAchievements/RABot-Next/releases/latest", @@ -42,7 +45,8 @@ describe("GithubReleaseService", () => { }); it("should return null when API returns non-ok status", async () => { - const mockFetch = mock(() => + // ARRANGE + const mockFetch = vi.fn(() => Promise.resolve({ ok: false, status: 404, @@ -52,66 +56,67 @@ describe("GithubReleaseService", () => { // @ts-expect-error - global.fetch is assignable global.fetch = mockFetch; + // ACT const version = await GithubReleaseService.fetchLatestVersion(); + // ASSERT expect(version).toBeNull(); }); it("should return null when fetch throws an error", async () => { - const mockFetch = mock(() => Promise.reject(new Error("Network error"))); + // ARRANGE + const mockFetch = vi.fn(() => Promise.reject(new Error("Network error"))); // @ts-expect-error - global.fetch is assignable global.fetch = mockFetch; + // ACT const version = await GithubReleaseService.fetchLatestVersion(); + // ASSERT expect(version).toBeNull(); }); it("should return cached version within cache duration", async () => { - const mockFetch = mock(() => + // ARRANGE + const mockFetch = vi.fn(() => Promise.resolve({ ok: true, status: 200, - json: mock(() => Promise.resolve({ tag_name: "2.0.1" })), + json: vi.fn(() => Promise.resolve({ tag_name: "2.0.1" })), }), ); // @ts-expect-error - global.fetch is assignable global.fetch = mockFetch; - // First call + // ACT const version1 = await GithubReleaseService.fetchLatestVersion(); - expect(version1).toBe("2.0.1"); - - // Second call (should use cache) const version2 = await GithubReleaseService.fetchLatestVersion(); - expect(version2).toBe("2.0.1"); - // Fetch should only be called once + // ASSERT + expect(version1).toBe("2.0.1"); + expect(version2).toBe("2.0.1"); expect(mockFetch).toHaveBeenCalledTimes(1); }); it("should refetch after cache expires", async () => { - const mockFetch = mock(() => + // ARRANGE + const mockFetch = vi.fn(() => Promise.resolve({ ok: true, status: 200, - json: mock(() => Promise.resolve({ tag_name: "2.0.1" })), + json: vi.fn(() => Promise.resolve({ tag_name: "2.0.1" })), }), ); // @ts-expect-error - global.fetch is assignable global.fetch = mockFetch; - - // First call await GithubReleaseService.fetchLatestVersion(); - - // Manually expire cache // @ts-expect-error - Accessing private property for testing GithubReleaseService.cacheTimestamp = Date.now() - (60 * 1000 + 1); - // Second call (should refetch) + // ACT await GithubReleaseService.fetchLatestVersion(); - // Fetch should be called twice + // ASSERT expect(mockFetch).toHaveBeenCalledTimes(2); }); }); diff --git a/src/services/poll.service.test.ts b/src/services/poll.service.test.ts index 7e55f0c..9f24329 100644 --- a/src/services/poll.service.test.ts +++ b/src/services/poll.service.test.ts @@ -1,24 +1,23 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createMockPoll, createMockPollVote } from "../test/mocks/database.mock"; import { PollService } from "./poll.service"; -// ... mock the database module ... -let mockDb: any; - -mock.module("../database/db", () => { - mockDb = { - select: mock(() => mockDb), - from: mock(() => mockDb), - where: mock(() => Promise.resolve([])), - insert: mock(() => mockDb), - values: mock(() => mockDb), - returning: mock(() => Promise.resolve([])), +const { mockDb } = vi.hoisted(() => { + const mockDb: any = { + select: vi.fn(() => mockDb), + from: vi.fn(() => mockDb), + where: vi.fn(() => Promise.resolve([])), + insert: vi.fn(() => mockDb), + values: vi.fn(() => mockDb), + returning: vi.fn(() => Promise.resolve([])), }; - return { db: mockDb }; + return { mockDb }; }); +vi.mock("../database/db", () => ({ db: mockDb })); + describe("Service: PollService", () => { beforeEach(() => { // ... reset all mocks before each test ... diff --git a/src/services/team.service.test.ts b/src/services/team.service.test.ts index 3b670db..144216f 100644 --- a/src/services/team.service.test.ts +++ b/src/services/team.service.test.ts @@ -1,26 +1,25 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createMockTeam, createMockTeamMember } from "../test/mocks/database.mock"; import { TeamService } from "./team.service"; -// ... mock the database module ... -let mockDb: any; - -mock.module("../database/db", () => { - mockDb = { - select: mock(() => mockDb), - from: mock(() => mockDb), - where: mock(() => Promise.resolve([])), - insert: mock(() => mockDb), - values: mock(() => mockDb), - returning: mock(() => Promise.resolve([])), - onConflictDoNothing: mock(() => Promise.resolve()), - delete: mock(() => mockDb), +const { mockDb } = vi.hoisted(() => { + const mockDb: any = { + select: vi.fn(() => mockDb), + from: vi.fn(() => mockDb), + where: vi.fn(() => Promise.resolve([])), + insert: vi.fn(() => mockDb), + values: vi.fn(() => mockDb), + returning: vi.fn(() => Promise.resolve([])), + onConflictDoNothing: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => mockDb), }; - return { db: mockDb }; + return { mockDb }; }); +vi.mock("../database/db", () => ({ db: mockDb })); + describe("Service: TeamService", () => { beforeEach(() => { // ... reset all mocks before each test ... diff --git a/src/services/template.service.test.ts b/src/services/template.service.test.ts index fd16c4b..ad5f76e 100644 --- a/src/services/template.service.test.ts +++ b/src/services/template.service.test.ts @@ -1,6 +1,6 @@ import type { GameExtended } from "@retroachievements/api"; -import { describe, expect, it } from "bun:test"; import type { User } from "discord.js"; +import { describe, expect, it } from "vitest"; import { TemplateService } from "./template.service"; diff --git a/src/services/uwc-poll.service.test.ts b/src/services/uwc-poll.service.test.ts index cbe4bfb..36e4131 100644 --- a/src/services/uwc-poll.service.test.ts +++ b/src/services/uwc-poll.service.test.ts @@ -1,14 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { db } from "../database/db"; import { uwcPollResults, uwcPolls } from "../database/schema"; import { UwcPollService } from "./uwc-poll.service"; -// Skip database-dependent tests in CI environment where Drizzle methods are undefined. -const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; -const describeOrSkip = isCI ? describe.skip : describe; - -describeOrSkip("UwcPollService", () => { +describe("UwcPollService", () => { // Clean up database after each test. afterEach(async () => { await db.delete(uwcPollResults); @@ -17,6 +13,7 @@ describeOrSkip("UwcPollService", () => { describe("createUwcPoll", () => { it("creates a new UWC poll", async () => { + // ARRANGE const pollData = { messageId: "123456789", channelId: "987654321", @@ -29,8 +26,10 @@ describeOrSkip("UwcPollService", () => { pollUrl: "https://discord.com/channels/123/456/789", }; + // ACT const poll = await UwcPollService.createUwcPoll(pollData); + // ASSERT expect(poll).toBeDefined(); expect(poll.messageId).toBe(pollData.messageId); expect(poll.channelId).toBe(pollData.channelId); @@ -41,6 +40,7 @@ describeOrSkip("UwcPollService", () => { }); it("creates a poll without optional fields", async () => { + // ARRANGE const pollData = { messageId: "123456789", channelId: "987654321", @@ -48,8 +48,10 @@ describeOrSkip("UwcPollService", () => { pollUrl: "https://discord.com/channels/123/456/789", }; + // ACT const poll = await UwcPollService.createUwcPoll(pollData); + // ASSERT expect(poll).toBeDefined(); expect(poll.threadId).toBeNull(); expect(poll.achievementId).toBeNull(); @@ -59,62 +61,53 @@ describeOrSkip("UwcPollService", () => { describe("getUwcPollByMessageId", () => { it("returns a poll by message ID", async () => { - const pollData = { + // ARRANGE + await UwcPollService.createUwcPoll({ messageId: "123456789", channelId: "987654321", creatorId: "111111111", pollUrl: "https://discord.com/channels/123/456/789", - }; + }); - await UwcPollService.createUwcPoll(pollData); + // ACT const poll = await UwcPollService.getUwcPollByMessageId("123456789"); + // ASSERT expect(poll).toBeDefined(); expect(poll?.messageId).toBe("123456789"); }); it("returns null for non-existent poll", async () => { + // ACT const poll = await UwcPollService.getUwcPollByMessageId("nonexistent"); + + // ASSERT expect(poll).toBeNull(); }); }); describe("completeUwcPoll", () => { it("completes a poll and stores results", async () => { - // Create a poll first. - const pollData = { + // ARRANGE + await UwcPollService.createUwcPoll({ messageId: "123456789", channelId: "987654321", creatorId: "111111111", pollUrl: "https://discord.com/channels/123/456/789", - }; - - await UwcPollService.createUwcPoll(pollData); - - // Complete the poll with results. + }); const results = [ - { - optionText: "No, leave as is", - voteCount: 5, - votePercentage: 50.0, - }, - { - optionText: "Yes, demote", - voteCount: 3, - votePercentage: 30.0, - }, - { - optionText: "Need further discussion", - voteCount: 2, - votePercentage: 20.0, - }, + { optionText: "No, leave as is", voteCount: 5, votePercentage: 50.0 }, + { optionText: "Yes, demote", voteCount: 3, votePercentage: 30.0 }, + { optionText: "Need further discussion", voteCount: 2, votePercentage: 20.0 }, ]; + // ACT const { poll, results: storedResults } = await UwcPollService.completeUwcPoll( "123456789", results, ); + // ASSERT expect(poll.status).toBe("completed"); expect(poll.endedAt).toBeDefined(); expect(storedResults).toHaveLength(3); @@ -123,34 +116,34 @@ describeOrSkip("UwcPollService", () => { }); it("throws error for non-existent poll", async () => { - expect(async () => { + // ACT & ASSERT + await expect(async () => { await UwcPollService.completeUwcPoll("nonexistent", []); - }).toThrow(); + }).rejects.toThrow(); }); }); describe("getActiveUwcPolls", () => { it("returns only active polls", async () => { - // Create an active poll. + // ARRANGE await UwcPollService.createUwcPoll({ messageId: "active1", channelId: "987654321", creatorId: "111111111", pollUrl: "https://discord.com/channels/123/456/789", }); - - // Create and complete another poll. await UwcPollService.createUwcPoll({ messageId: "completed1", channelId: "987654321", creatorId: "111111111", pollUrl: "https://discord.com/channels/123/456/789", }); - await UwcPollService.completeUwcPoll("completed1", []); + // ACT const activePolls = await UwcPollService.getActiveUwcPolls(); + // ASSERT expect(activePolls).toHaveLength(1); expect(activePolls[0]?.messageId).toBe("active1"); }); @@ -158,7 +151,7 @@ describeOrSkip("UwcPollService", () => { describe("getUwcPollsByAchievement", () => { it("returns polls for a specific achievement", async () => { - // Create polls for different achievements. + // ARRANGE await UwcPollService.createUwcPoll({ messageId: "poll1", channelId: "987654321", @@ -167,7 +160,6 @@ describeOrSkip("UwcPollService", () => { achievementName: "Test Achievement", pollUrl: "https://discord.com/channels/123/456/789", }); - await UwcPollService.createUwcPoll({ messageId: "poll2", channelId: "987654321", @@ -176,7 +168,6 @@ describeOrSkip("UwcPollService", () => { achievementName: "Test Achievement", pollUrl: "https://discord.com/channels/123/456/789", }); - await UwcPollService.createUwcPoll({ messageId: "poll3", channelId: "987654321", @@ -186,8 +177,10 @@ describeOrSkip("UwcPollService", () => { pollUrl: "https://discord.com/channels/123/456/789", }); + // ACT const polls = await UwcPollService.getUwcPollsByAchievement(14402); + // ASSERT expect(polls).toHaveLength(2); expect(polls.every((p) => p.achievementId === 14402)).toBe(true); }); @@ -220,19 +213,28 @@ describeOrSkip("UwcPollService", () => { }); it("searches by achievement name", async () => { + // ACT const polls = await UwcPollService.searchUwcPolls("sonic"); + + // ASSERT expect(polls).toHaveLength(1); expect(polls[0]?.achievementName).toBe("Sonic Speed"); }); it("searches by game name", async () => { + // ACT const polls = await UwcPollService.searchUwcPolls("mario"); + + // ASSERT expect(polls).toHaveLength(1); expect(polls[0]?.gameName).toBe("Super Mario Bros."); }); it("returns empty array for no matches", async () => { + // ACT const polls = await UwcPollService.searchUwcPolls("zelda"); + + // ASSERT expect(polls).toHaveLength(0); }); }); diff --git a/src/services/youtube.service.test.ts b/src/services/youtube.service.test.ts index e7f4f21..6f83add 100644 --- a/src/services/youtube.service.test.ts +++ b/src/services/youtube.service.test.ts @@ -1,42 +1,43 @@ -import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import * as logger from "../utils/logger"; import { YouTubeService } from "./youtube.service"; -// ... mock the youtube-search module ... -const mockYtSearch = mock(async (searchTerms, _opts) => { - if (searchTerms.includes("error game")) { - throw new Error("YouTube API Error"); - } - if (searchTerms.includes("no results")) { - return { results: [] }; - } - - return { - results: [ - { - link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - title: "Test Game Longplay", - description: "A longplay of Test Game", - }, - ], - }; -}); +const { mockYtSearch } = vi.hoisted(() => ({ + mockYtSearch: vi.fn(), +})); -mock.module("youtube-search", () => ({ +vi.mock("youtube-search", () => ({ default: mockYtSearch, })); // ... mock constants ... -mock.module("../config/constants", () => ({ +vi.mock("../config/constants", () => ({ YOUTUBE_API_KEY: "test-youtube-api-key", })); describe("Service: YouTubeService", () => { beforeEach(() => { - // ... reset mocks ... - mockYtSearch.mockClear(); - spyOn(logger, "logError").mockImplementation(() => {}); + mockYtSearch.mockClear().mockImplementation(async (searchTerms: string) => { + if (searchTerms.includes("error game")) { + throw new Error("YouTube API Error"); + } + if (searchTerms.includes("no results")) { + return { results: [] }; + } + + return { + results: [ + { + link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + title: "Test Game Longplay", + description: "A longplay of Test Game", + }, + ], + }; + }); + + vi.spyOn(logger, "logError").mockImplementation(() => {}); }); describe("searchLongplay", () => { @@ -135,12 +136,10 @@ describe("Service: YouTubeService", () => { it("returns null when YOUTUBE_API_KEY is not set", async () => { // ARRANGE - // ... temporarily mock the constants module without API key ... - mock.module("../config/constants", () => ({ + vi.resetModules(); + vi.doMock("../config/constants", () => ({ YOUTUBE_API_KEY: undefined, })); - - // ... need to re-import to get the new mock ... const { YouTubeService: YTServiceNoKey } = await import("./youtube.service"); // ACT @@ -149,11 +148,6 @@ describe("Service: YouTubeService", () => { // ASSERT expect(mockYtSearch).not.toHaveBeenCalled(); expect(result).toBeNull(); - - // ... restore the original mock ... - mock.module("../config/constants", () => ({ - YOUTUBE_API_KEY: "test-youtube-api-key", - })); }); }); }); diff --git a/src/slash-commands/frames.command.test.ts b/src/slash-commands/frames.command.test.ts index 5c5ee9b..3200c7e 100644 --- a/src/slash-commands/frames.command.test.ts +++ b/src/slash-commands/frames.command.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { FramesService } from "../services/frames.service"; import { createMockInteraction } from "../test/mocks/discord.mock"; @@ -11,13 +11,13 @@ describe("SlashCommand: frames", () => { mockInteraction = createMockInteraction({ commandName: "frames", options: { - getString: mock(() => "1h"), + getString: vi.fn(() => "1h"), }, }); }); afterEach(() => { - mock.restore(); + vi.restoreAllMocks(); }); it("is defined", () => { @@ -29,8 +29,8 @@ describe("SlashCommand: frames", () => { describe("execute", () => { it("shows error message for invalid input", async () => { // ARRANGE - mockInteraction.options.getString = mock(() => "invalid input"); - spyOn(FramesService, "processInput").mockReturnValue(null); + mockInteraction.options.getString = vi.fn(() => "invalid input"); + vi.spyOn(FramesService, "processInput").mockReturnValue(null); // ACT await framesSlashCommand.execute(mockInteraction, {} as any); @@ -46,8 +46,8 @@ describe("SlashCommand: frames", () => { // ARRANGE const expectedOutput = "**Time:** `1h 0min 0s 0ms`\n**FPS:** `60`\n**Frames:** `216000 (0x34bc0)`"; - mockInteraction.options.getString = mock(() => "1h"); - spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); + mockInteraction.options.getString = vi.fn(() => "1h"); + vi.spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); // ACT await framesSlashCommand.execute(mockInteraction, {} as any); @@ -61,8 +61,8 @@ describe("SlashCommand: frames", () => { // ARRANGE const expectedOutput = "**Time:** `0h 1min 0s 0ms`\n**FPS:** `60`\n**Frames:** `3600 (0xe10)`"; - mockInteraction.options.getString = mock(() => "3600"); - spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); + mockInteraction.options.getString = vi.fn(() => "3600"); + vi.spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); // ACT await framesSlashCommand.execute(mockInteraction, {} as any); @@ -76,8 +76,8 @@ describe("SlashCommand: frames", () => { // ARRANGE const expectedOutput = "**Time:** `0h 0min 30s 0ms`\n**FPS:** `30`\n**Frames:** `900 (0x384)`"; - mockInteraction.options.getString = mock(() => "30s 30fps"); - spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); + mockInteraction.options.getString = vi.fn(() => "30s 30fps"); + vi.spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); // ACT await framesSlashCommand.execute(mockInteraction, {} as any); @@ -91,8 +91,8 @@ describe("SlashCommand: frames", () => { // ARRANGE const expectedOutput = "**Time:** `0h 2min 0s 0ms`\n**FPS:** `60`\n**Frames:** `7200 (0x1c20)`"; - mockInteraction.options.getString = mock(() => "2min"); - spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); + mockInteraction.options.getString = vi.fn(() => "2min"); + vi.spyOn(FramesService, "processInput").mockReturnValue(expectedOutput); // ACT await framesSlashCommand.execute(mockInteraction, {} as any); diff --git a/src/slash-commands/pingteam.command.test.ts b/src/slash-commands/pingteam.command.test.ts index f224c60..5436d91 100644 --- a/src/slash-commands/pingteam.command.test.ts +++ b/src/slash-commands/pingteam.command.test.ts @@ -1,5 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; import { ChannelType, MessageFlags } from "discord.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { CHEAT_INVESTIGATION_CATEGORY_ID, WORKSHOP_GUILD_ID } from "../config/constants"; import { TeamService } from "../services/team.service"; @@ -13,15 +13,15 @@ import pingteamSlashCommand from "./pingteam.command"; describe("SlashCommand: pingteam", () => { beforeEach(() => { // Spy on TeamService methods and provide default mock implementations. - spyOn(TeamService, "getTeamMembersByName").mockResolvedValue([]); - spyOn(TeamService, "addMemberByTeamName").mockResolvedValue(); - spyOn(TeamService, "removeMemberByTeamName").mockResolvedValue(true); - spyOn(TeamService, "createTeam").mockResolvedValue({} as any); + vi.spyOn(TeamService, "getTeamMembersByName").mockResolvedValue([]); + vi.spyOn(TeamService, "addMemberByTeamName").mockResolvedValue(); + vi.spyOn(TeamService, "removeMemberByTeamName").mockResolvedValue(true); + vi.spyOn(TeamService, "createTeam").mockResolvedValue({} as any); }); afterEach(() => { // Restore all spies to prevent test pollution. - mock.restore(); + vi.restoreAllMocks(); }); describe("ping subcommand", () => { @@ -34,8 +34,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel: null, // DM channel options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "racheats"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "racheats"), }, }); @@ -57,8 +57,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel: { type: ChannelType.DM }, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "racheats"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "racheats"), }, }); @@ -85,8 +85,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "racheats"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "racheats"), }, }); @@ -112,8 +112,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "racheats"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "racheats"), }, }); @@ -140,8 +140,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "RaChEaTs"), // Mixed case + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "RaChEaTs"), // Mixed case }, }); @@ -168,8 +168,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel: threadChannel, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "racheats"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "racheats"), }, }); @@ -196,8 +196,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel: threadChannel, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "racheats"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "racheats"), }, }); @@ -224,8 +224,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel: threadChannel, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "racheats"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "racheats"), }, }); @@ -252,8 +252,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel: threadChannel, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "racheats"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "racheats"), }, }); @@ -281,8 +281,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel: threadChannel, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "racheats"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "racheats"), }, }); @@ -310,8 +310,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel: threadChannel, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "racheats"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "racheats"), }, }); @@ -341,8 +341,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel, options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "moderators"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "moderators"), }, }); @@ -373,9 +373,9 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel, options: { - getSubcommand: mock(() => "add"), - getString: mock(() => "racheats"), - getUser: mock(() => mockUser), + getSubcommand: vi.fn(() => "add"), + getString: vi.fn(() => "racheats"), + getUser: vi.fn(() => mockUser), }, }); @@ -403,9 +403,9 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel, options: { - getSubcommand: mock(() => "remove"), - getString: mock(() => "racheats"), - getUser: mock(() => mockUser), + getSubcommand: vi.fn(() => "remove"), + getString: vi.fn(() => "racheats"), + getUser: vi.fn(() => mockUser), }, }); @@ -428,8 +428,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel, options: { - getSubcommand: mock(() => "create"), - getString: mock(() => "newteam"), + getSubcommand: vi.fn(() => "create"), + getString: vi.fn(() => "newteam"), }, }); @@ -452,9 +452,9 @@ describe("SlashCommand: pingteam", () => { commandName: "pingteam", guildId: "999999999999999999", // Different guild options: { - getSubcommand: mock(() => subcommand), - getString: mock(() => "testteam"), - getUser: mock(() => ({ id: "user123" })), + getSubcommand: vi.fn(() => subcommand), + getString: vi.fn(() => "testteam"), + getUser: vi.fn(() => ({ id: "user123" })), }, }); @@ -474,11 +474,11 @@ describe("SlashCommand: pingteam", () => { expect(TeamService.createTeam).not.toHaveBeenCalled(); // Reset mocks for next iteration - mock.restore(); - spyOn(TeamService, "getTeamMembersByName").mockResolvedValue([]); - spyOn(TeamService, "addMemberByTeamName").mockResolvedValue(); - spyOn(TeamService, "removeMemberByTeamName").mockResolvedValue(true); - spyOn(TeamService, "createTeam").mockResolvedValue({} as any); + vi.restoreAllMocks(); + vi.spyOn(TeamService, "getTeamMembersByName").mockResolvedValue([]); + vi.spyOn(TeamService, "addMemberByTeamName").mockResolvedValue(); + vi.spyOn(TeamService, "removeMemberByTeamName").mockResolvedValue(true); + vi.spyOn(TeamService, "createTeam").mockResolvedValue({} as any); } }); @@ -488,8 +488,8 @@ describe("SlashCommand: pingteam", () => { commandName: "pingteam", guildId: null, // DM - no guild ID options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "testteam"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "testteam"), }, }); @@ -511,8 +511,8 @@ describe("SlashCommand: pingteam", () => { guildId: WORKSHOP_GUILD_ID, channel: createMockTextChannel({} as any), options: { - getSubcommand: mock(() => "ping"), - getString: mock(() => "testteam"), + getSubcommand: vi.fn(() => "ping"), + getString: vi.fn(() => "testteam"), }, }); @@ -528,16 +528,16 @@ describe("SlashCommand: pingteam", () => { ); // Reset for next test - mock.restore(); - spyOn(TeamService, "createTeam").mockResolvedValue({} as any); + vi.restoreAllMocks(); + vi.spyOn(TeamService, "createTeam").mockResolvedValue({} as any); // ARRANGE - Test create subcommand const createInteraction = createMockInteraction({ commandName: "pingteam", guildId: WORKSHOP_GUILD_ID, options: { - getSubcommand: mock(() => "create"), - getString: mock(() => "newteam"), + getSubcommand: vi.fn(() => "create"), + getString: vi.fn(() => "newteam"), }, }); diff --git a/src/slash-commands/uwc.command.test.ts b/src/slash-commands/uwc.command.test.ts index 0059fc9..ee98012 100644 --- a/src/slash-commands/uwc.command.test.ts +++ b/src/slash-commands/uwc.command.test.ts @@ -1,5 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; import { ChannelType, MessageFlags, PermissionFlagsBits } from "discord.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { WORKSHOP_GUILD_ID } from "../config/constants"; import { db } from "../database/db"; @@ -8,24 +8,24 @@ import { UwcPollService } from "../services/uwc-poll.service"; import { createMockGuildMember, createMockInteraction } from "../test/mocks/discord.mock"; import uwcSlashCommand from "./uwc.command"; -// Skip database-dependent tests in CI environment where Drizzle methods are undefined. -const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; -const describeOrSkip = isCI ? describe.skip : describe; +const { MOCK_UWC_VOTING_TAG_ID, MOCK_UWC_VOTE_CONCLUDED_TAG_ID } = vi.hoisted(() => ({ + MOCK_UWC_VOTING_TAG_ID: "mockVotingTag123", + MOCK_UWC_VOTE_CONCLUDED_TAG_ID: "mockConcludedTag123", +})); -// Mock the tag IDs for testing -const MOCK_UWC_VOTING_TAG_ID = "mockVotingTag123"; -const MOCK_UWC_VOTE_CONCLUDED_TAG_ID = "mockConcludedTag123"; +vi.mock("../config/constants", async (importOriginal) => { + const actual: Record = await importOriginal(); -// Replace the constants module -mock.module("../config/constants", () => ({ - WORKSHOP_GUILD_ID, - UWC_VOTING_TAG_ID: MOCK_UWC_VOTING_TAG_ID, - UWC_VOTE_CONCLUDED_TAG_ID: MOCK_UWC_VOTE_CONCLUDED_TAG_ID, -})); + return { + ...actual, + UWC_VOTING_TAG_ID: MOCK_UWC_VOTING_TAG_ID, + UWC_VOTE_CONCLUDED_TAG_ID: MOCK_UWC_VOTE_CONCLUDED_TAG_ID, + }; +}); const UWC_ROLE_ID = "1002687198757388299"; -describeOrSkip("SlashCommand: uwc", () => { +describe("SlashCommand: uwc", () => { beforeEach(async () => { // Clean up database before each test. await db.delete(uwcPollResults); @@ -33,7 +33,7 @@ describeOrSkip("SlashCommand: uwc", () => { }); afterEach(() => { - mock.restore(); + vi.restoreAllMocks(); }); describe("guild restrictions", () => { @@ -76,7 +76,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), // Has required role + has: vi.fn(() => true), // Has required role }, }, }); @@ -135,7 +135,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => false), // No required role + has: vi.fn(() => false), // No required role }, }, }); @@ -145,7 +145,7 @@ describeOrSkip("SlashCommand: uwc", () => { guildId: WORKSHOP_GUILD_ID, member, memberPermissions: { - has: mock(() => false), // No admin permission + has: vi.fn(() => false), // No admin permission }, }); @@ -164,7 +164,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock((roleId: string) => roleId === UWC_ROLE_ID), // Has required role + has: vi.fn((roleId: string) => roleId === UWC_ROLE_ID), // Has required role }, }, }); @@ -174,7 +174,7 @@ describeOrSkip("SlashCommand: uwc", () => { guildId: WORKSHOP_GUILD_ID, member, memberPermissions: { - has: mock(() => false), // No admin permission needed + has: vi.fn(() => false), // No admin permission needed }, }); @@ -206,7 +206,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => false), // No required role + has: vi.fn(() => false), // No required role }, }, }); @@ -216,7 +216,7 @@ describeOrSkip("SlashCommand: uwc", () => { guildId: WORKSHOP_GUILD_ID, member, memberPermissions: { - has: mock((permission) => permission === PermissionFlagsBits.Administrator), // Has admin + has: vi.fn((permission) => permission === PermissionFlagsBits.Administrator), // Has admin }, }); @@ -254,7 +254,7 @@ describeOrSkip("SlashCommand: uwc", () => { guildId: WORKSHOP_GUILD_ID, member, memberPermissions: { - has: mock(() => false), // No admin permission + has: vi.fn(() => false), // No admin permission }, }); @@ -275,7 +275,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), // Has required role + has: vi.fn(() => true), // Has required role }, }, }); @@ -314,7 +314,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); @@ -338,7 +338,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); @@ -364,7 +364,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); @@ -393,7 +393,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); @@ -406,7 +406,7 @@ describeOrSkip("SlashCommand: uwc", () => { // Mock the database to throw an error const originalCreateUwcPoll = UwcPollService.createUwcPoll; - UwcPollService.createUwcPoll = mock(() => { + UwcPollService.createUwcPoll = vi.fn(() => { throw new Error("Database error"); }); @@ -427,7 +427,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); @@ -441,7 +441,7 @@ describeOrSkip("SlashCommand: uwc", () => { type: ChannelType.PublicThread, name: "243323: I Guess Two Heads Aren't That Great After All (Ys: The Vanished Omens)", appliedTags: [], - setAppliedTags: mock(() => Promise.resolve()), + setAppliedTags: vi.fn(() => Promise.resolve()), }, }); @@ -460,7 +460,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); @@ -474,7 +474,7 @@ describeOrSkip("SlashCommand: uwc", () => { type: ChannelType.PublicThread, name: "136277: Eternal Champion XIII (Dalles) (Ys: Book I & II)", appliedTags: [], - setAppliedTags: mock(() => Promise.resolve()), + setAppliedTags: vi.fn(() => Promise.resolve()), }, }); @@ -493,7 +493,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); @@ -507,7 +507,7 @@ describeOrSkip("SlashCommand: uwc", () => { type: ChannelType.PublicThread, name: "123456: Test Achievement (Game Name (with parentheses))", appliedTags: [], - setAppliedTags: mock(() => Promise.resolve()), + setAppliedTags: vi.fn(() => Promise.resolve()), }, }); @@ -526,7 +526,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); @@ -540,7 +540,7 @@ describeOrSkip("SlashCommand: uwc", () => { type: ChannelType.PublicThread, name: "14402: Achievement without game info", appliedTags: [], - setAppliedTags: mock(() => Promise.resolve()), + setAppliedTags: vi.fn(() => Promise.resolve()), }, }); @@ -559,7 +559,7 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); @@ -592,12 +592,12 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); - const mockSetAppliedTags = mock(() => Promise.resolve()); + const mockSetAppliedTags = vi.fn(() => Promise.resolve()); const interaction = createMockInteraction({ commandName: "uwc", guildId: WORKSHOP_GUILD_ID, @@ -623,12 +623,12 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); - const mockSetAppliedTags = mock(() => Promise.resolve()); + const mockSetAppliedTags = vi.fn(() => Promise.resolve()); const interaction = createMockInteraction({ commandName: "uwc", guildId: WORKSHOP_GUILD_ID, @@ -654,12 +654,12 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); - const mockSetAppliedTags = mock(() => { + const mockSetAppliedTags = vi.fn(() => { throw new Error("Permission denied"); }); @@ -689,12 +689,12 @@ describeOrSkip("SlashCommand: uwc", () => { const member = createMockGuildMember({ roles: { cache: { - has: mock(() => true), + has: vi.fn(() => true), }, }, }); - const mockSetAppliedTags = mock(() => Promise.resolve()); + const mockSetAppliedTags = vi.fn(() => Promise.resolve()); const interaction = createMockInteraction({ commandName: "uwc", guildId: WORKSHOP_GUILD_ID, diff --git a/src/test/mocks/database.mock.ts b/src/test/mocks/database.mock.ts index 5e208f8..d1cd3e5 100644 --- a/src/test/mocks/database.mock.ts +++ b/src/test/mocks/database.mock.ts @@ -1,5 +1,3 @@ -import { mock } from "bun:test"; - import type { teamMembers, teams } from "../../database/schema"; type Team = typeof teams.$inferSelect; @@ -51,119 +49,3 @@ export function createMockPollVote(overrides?: any) { ...overrides, }; } - -/** - * Applies database method mocks conditionally - only mocks methods that don't exist. - * This is useful for CI environments where Drizzle methods may be undefined. - * - * @param dbInstance - The database instance to potentially mock - * @param mockData - Optional mock data to return from database operations - */ -export const applyConditionalDbMocks = (dbInstance: any, mockData?: any) => { - // Mock database storage to simulate real database behavior. - const mockStorage = new Map(); - let idCounter = 1; - - const _defaultMockData = { - id: 1, - messageId: "123456789", - channelId: "987654321", - status: "active", - startedAt: new Date(), - endedAt: null, - ...mockData, - }; - - const mockDbMethods = { - transaction: mock(async (callback: any) => callback(dbInstance)), - delete: mock(() => ({ - where: mock(() => { - mockStorage.clear(); - - return Promise.resolve(); - }), - })), - insert: mock(() => ({ - values: mock((data: any) => ({ - returning: mock(() => { - const newRecord = Array.isArray(data) ? data[0] : data; - const recordWithId = { - id: idCounter++, - ...newRecord, - startedAt: new Date(), - status: newRecord.status || "active", - }; - mockStorage.set(recordWithId.messageId || recordWithId.id, recordWithId); - - return Promise.resolve([recordWithId]); - }), - })), - })), - select: mock(() => ({ - from: mock(() => ({ - where: mock((_condition: any) => { - // Try to extract messageId from condition for more realistic behavior. - const records = Array.from(mockStorage.values()); - - return Promise.resolve(records); - }), - orderBy: mock(() => { - const records = Array.from(mockStorage.values()); - - return Promise.resolve(records); - }), - })), - })), - update: mock(() => ({ - set: mock(() => ({ - where: mock(() => ({ - returning: mock(() => { - const records = Array.from(mockStorage.values()); - const updatedRecords = records.map((record) => ({ - ...record, - status: "completed", - endedAt: new Date(), - })); - - return Promise.resolve(updatedRecords); - }), - })), - })), - })), - }; - - // Only mock methods that don't exist (CI environment). - for (const [method, mockFn] of Object.entries(mockDbMethods)) { - if (!dbInstance[method]) { - dbInstance[method] = mockFn; - } - } - - return dbInstance; -}; - -// Legacy mock database instance for testing (kept for backward compatibility). -export const createMockDb = () => { - const mockTransactionFn = mock(async (callback: any) => { - // Execute the callback with a mock transaction object. - return callback({ - select: mock(() => ({ from: mock(() => ({ where: mock(() => []) })) })), - insert: mock(() => ({ values: mock(() => ({ returning: mock(() => []) })) })), - update: mock(() => ({ - set: mock(() => ({ where: mock(() => ({ returning: mock(() => []) })) })), - })), - delete: mock(() => ({ where: mock(() => []) })), - }); - }); - - return { - select: mock(() => ({ from: mock(() => ({ where: mock(() => []) })) })), - insert: mock(() => ({ values: mock(() => ({ returning: mock(() => []) })) })), - update: mock(() => ({ - set: mock(() => ({ where: mock(() => ({ returning: mock(() => []) })) })), - })), - delete: mock(() => ({ where: mock(() => []) })), - transaction: mockTransactionFn, - run: mock(() => Promise.resolve()), - }; -}; diff --git a/src/test/mocks/discord.mock.ts b/src/test/mocks/discord.mock.ts index 2d639aa..e56faed 100644 --- a/src/test/mocks/discord.mock.ts +++ b/src/test/mocks/discord.mock.ts @@ -1,4 +1,3 @@ -import { type Mock, mock } from "bun:test"; import { ChannelType, type ChatInputCommandInteraction, @@ -10,6 +9,7 @@ import { type TextChannel, type User, } from "discord.js"; +import { type Mock, vi } from "vitest"; import type { BotClient } from "../../models"; @@ -24,7 +24,7 @@ export function createMockUser(overrides?: any): User { avatar: null, banner: null, accentColor: null, - displayAvatarURL: mock(() => "https://example.com/avatar.png"), + displayAvatarURL: vi.fn(() => "https://example.com/avatar.png"), ...overrides, } as User; } @@ -46,8 +46,8 @@ export function createMockTextChannel(overrides?: Partial): TextCha id: "111111111", type: ChannelType.GuildText, name: "test-channel", - send: mock(() => Promise.resolve({ id: "msg123" })), - permissionsFor: mock(() => new PermissionsBitField(["SendMessages", "EmbedLinks"])), + send: vi.fn(() => Promise.resolve({ id: "msg123" })), + permissionsFor: vi.fn(() => new PermissionsBitField(["SendMessages", "EmbedLinks"])), parentId: null, parent: null, ...overrides, @@ -67,8 +67,8 @@ export function createMockThreadChannel(overrides?: any): any { name: "test-thread", parentId: parentChannel.id, parent: parentChannel, - send: mock(() => Promise.resolve({ id: "msg123" })), - permissionsFor: mock(() => new PermissionsBitField(["SendMessages", "EmbedLinks"])), + send: vi.fn(() => Promise.resolve({ id: "msg123" })), + permissionsFor: vi.fn(() => new PermissionsBitField(["SendMessages", "EmbedLinks"])), ...overrides, }; } @@ -114,10 +114,10 @@ export function createMockMessage(overrides?: any): Message { cache: new Collection(), }, reference: null, - reply: mock(() => Promise.resolve({} as Message)) as Mock<() => Promise>, - delete: mock(() => Promise.resolve({} as Message)), - react: mock(() => Promise.resolve({})), - edit: mock(() => Promise.resolve({} as Message)) as Mock<() => Promise>, + reply: vi.fn(() => Promise.resolve({} as Message)) as Mock<() => Promise>, + delete: vi.fn(() => Promise.resolve({} as Message)), + react: vi.fn(() => Promise.resolve({})), + edit: vi.fn(() => Promise.resolve({} as Message)) as Mock<() => Promise>, }; // Apply overrides @@ -148,29 +148,29 @@ export function createMockInteraction(overrides?: any): ChatInputCommandInteract user: createMockUser({ id: "bot123", bot: true }), }, options: { - getString: mock(() => null) as Mock<(name: string, required?: boolean) => string | null>, - getInteger: mock(() => null), - getBoolean: mock(() => null), - getUser: mock(() => null), - getMember: mock(() => null), - getChannel: mock(() => null), - getSubcommand: mock(() => null), - getSubcommandGroup: mock(() => null), + getString: vi.fn(() => null) as Mock<(name: string, required?: boolean) => string | null>, + getInteger: vi.fn(() => null), + getBoolean: vi.fn(() => null), + getUser: vi.fn(() => null), + getMember: vi.fn(() => null), + getChannel: vi.fn(() => null), + getSubcommand: vi.fn(() => null), + getSubcommandGroup: vi.fn(() => null), }, deferred: false, ephemeral: null, replied: false, - reply: mock((options: any) => { + reply: vi.fn((options: any) => { if (options?.fetchReply) { return Promise.resolve({ id: "pollMessage123" } as Message); } return Promise.resolve(); }), - deferReply: mock(() => Promise.resolve()), - editReply: mock(() => Promise.resolve({} as Message)), - deleteReply: mock(() => Promise.resolve()), - followUp: mock(() => Promise.resolve()), + deferReply: vi.fn(() => Promise.resolve()), + editReply: vi.fn(() => Promise.resolve({} as Message)), + deleteReply: vi.fn(() => Promise.resolve()), + followUp: vi.fn(() => Promise.resolve()), }; // Apply overrides diff --git a/src/utils/admin-checker.test.ts b/src/utils/admin-checker.test.ts index 0d93085..7d816bb 100644 --- a/src/utils/admin-checker.test.ts +++ b/src/utils/admin-checker.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "bun:test"; import type { ChatInputCommandInteraction, Message } from "discord.js"; +import { describe, expect, it } from "vitest"; import { AdminChecker } from "./admin-checker"; diff --git a/src/utils/command-analytics.test.ts b/src/utils/command-analytics.test.ts index e92af13..6470f7f 100644 --- a/src/utils/command-analytics.test.ts +++ b/src/utils/command-analytics.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, spyOn } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createMockInteraction, createMockMessage } from "../test/mocks/discord.mock"; import { CommandAnalytics } from "./command-analytics"; @@ -10,7 +10,7 @@ describe("Util: CommandAnalytics", () => { CommandAnalytics.reset(); // ... spy on logger ... - spyOn(logger, "info").mockImplementation(() => {}); + vi.spyOn(logger, "info").mockImplementation(() => {}); }); describe("startTracking", () => { diff --git a/src/utils/cooldown-manager.test.ts b/src/utils/cooldown-manager.test.ts index c913ba1..6994226 100644 --- a/src/utils/cooldown-manager.test.ts +++ b/src/utils/cooldown-manager.test.ts @@ -1,5 +1,5 @@ -import { beforeEach, describe, expect, it } from "bun:test"; import { Collection } from "discord.js"; +import { beforeEach, describe, expect, it } from "vitest"; import { CooldownManager } from "./cooldown-manager"; diff --git a/src/utils/error-tracker.test.ts b/src/utils/error-tracker.test.ts index 7aa6eb5..994af8c 100644 --- a/src/utils/error-tracker.test.ts +++ b/src/utils/error-tracker.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, spyOn } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createMockInteraction, createMockMessage } from "../test/mocks/discord.mock"; import { ErrorTracker } from "./error-tracker"; @@ -7,7 +7,7 @@ import * as logger from "./logger"; describe("Util: ErrorTracker", () => { beforeEach(() => { // ... spy on logger functions ... - spyOn(logger, "logError").mockImplementation(() => {}); + vi.spyOn(logger, "logError").mockImplementation(() => {}); }); describe("trackMessageError", () => { diff --git a/src/utils/guild-manager.test.ts b/src/utils/guild-manager.test.ts index 9ba896b..6d8f450 100644 --- a/src/utils/guild-manager.test.ts +++ b/src/utils/guild-manager.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it, mock } from "bun:test"; import type { Guild } from "discord.js"; +import { describe, expect, it, vi } from "vitest"; import { checkAndLeaveUnauthorizedGuilds, @@ -57,7 +57,7 @@ describe("Util: guild-manager", () => { const mockGuild = { id: "999999999999999999", name: "Unauthorized Guild", - leave: mock().mockResolvedValue(undefined), + leave: vi.fn().mockResolvedValue(undefined), } as unknown as Guild; // ACT @@ -73,7 +73,7 @@ describe("Util: guild-manager", () => { const mockGuild = { id: "999999999999999999", name: "Problematic Guild", - leave: mock().mockRejectedValue(error), + leave: vi.fn().mockRejectedValue(error), } as unknown as Guild; // ACT & ASSERT - Should not throw @@ -105,7 +105,7 @@ describe("Util: guild-manager", () => { { id: "999999999999999999", name: "Test Guild", - leave: mock(), + leave: vi.fn(), }, ] as unknown as Guild[]; @@ -128,12 +128,12 @@ describe("Util: guild-manager", () => { { id: "999999999999999999", // Likely unauthorized in most configs name: "Test Guild 1", - leave: mock().mockResolvedValue(undefined), + leave: vi.fn().mockResolvedValue(undefined), }, { id: "888888888888888888", // Likely unauthorized in most configs name: "Test Guild 2", - leave: mock().mockResolvedValue(undefined), + leave: vi.fn().mockResolvedValue(undefined), }, ] as unknown as Guild[]; @@ -157,7 +157,7 @@ describe("Util: guild-manager", () => { { id: "999999999999999999", name: "Test Guild", - leave: mock().mockResolvedValue(undefined), + leave: vi.fn().mockResolvedValue(undefined), }, ] as unknown as Guild[]; @@ -171,12 +171,12 @@ describe("Util: guild-manager", () => { { id: "310192285306454017", // Main RA guild name: "RetroAchievements", - leave: mock(), + leave: vi.fn(), }, { id: "476211979464343552", // Workshop guild name: "RA Workshop", - leave: mock(), + leave: vi.fn(), }, ] as unknown as Guild[]; diff --git a/src/utils/guild-restrictions.test.ts b/src/utils/guild-restrictions.test.ts index 92cd371..c11db1f 100644 --- a/src/utils/guild-restrictions.test.ts +++ b/src/utils/guild-restrictions.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it, mock } from "bun:test"; import { type ChatInputCommandInteraction, MessageFlags } from "discord.js"; +import { describe, expect, it, vi } from "vitest"; import { requireGuild } from "./guild-restrictions"; @@ -14,7 +14,7 @@ describe("Util: guild-restrictions", () => { // ARRANGE const mockInteraction = { guildId: "123456789012345678", - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -29,7 +29,7 @@ describe("Util: guild-restrictions", () => { // ARRANGE const mockInteraction = { guildId: "123456789012345678", - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -47,7 +47,7 @@ describe("Util: guild-restrictions", () => { // ARRANGE const mockInteraction = { guildId: null, - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -65,7 +65,7 @@ describe("Util: guild-restrictions", () => { // ARRANGE const mockInteraction = { guildId: undefined, - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -84,7 +84,7 @@ describe("Util: guild-restrictions", () => { const customMessage = "This command is restricted to specific servers."; const mockInteraction = { guildId: "wrong-guild-id", - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -102,7 +102,7 @@ describe("Util: guild-restrictions", () => { // ARRANGE const mockInteraction = { guildId: "wrong-guild-id", - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -120,7 +120,7 @@ describe("Util: guild-restrictions", () => { // ARRANGE const mockInteraction = { guildId: "", - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -138,7 +138,7 @@ describe("Util: guild-restrictions", () => { // ARRANGE const mockInteraction = { guildId: "123456789012345678", - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -156,7 +156,7 @@ describe("Util: guild-restrictions", () => { // ARRANGE const mockInteraction = { guildId: "123456789012345678", - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -175,7 +175,7 @@ describe("Util: guild-restrictions", () => { let replyResolved = false; const mockInteraction = { guildId: "wrong-guild", - reply: mock(async () => { + reply: vi.fn(async () => { replyResolved = true; return Promise.resolve(); @@ -195,7 +195,7 @@ describe("Util: guild-restrictions", () => { // ARRANGE const mockInteraction = { guildId: "wrong-guild", - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -213,7 +213,7 @@ describe("Util: guild-restrictions", () => { const realGuildId = "310192285306454017"; // Main RA server ID const mockInteraction = { guildId: realGuildId, - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -229,7 +229,7 @@ describe("Util: guild-restrictions", () => { const workshopGuildId = "476211979464343552"; // Workshop server ID const mockInteraction = { guildId: workshopGuildId, - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -246,7 +246,7 @@ describe("Util: guild-restrictions", () => { const workshopGuildId = "476211979464343552"; const mockInteraction = { guildId: mainGuildId, - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT @@ -266,7 +266,7 @@ describe("Util: guild-restrictions", () => { const workshopGuildId = "476211979464343552"; const mockInteraction = { guildId: workshopGuildId, - reply: mock(), + reply: vi.fn(), } as unknown as ChatInputCommandInteraction; // ACT diff --git a/src/utils/memory-parser.test.ts b/src/utils/memory-parser.test.ts index 65ba254..602a3a1 100644 --- a/src/utils/memory-parser.test.ts +++ b/src/utils/memory-parser.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "bun:test"; +import { describe, expect, it } from "vitest"; import { formatMemoryGroups, diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..fa11071 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + isolate: true, + }, +});