From cb4b7102b819b27fbebc71e08c9baede639db39a Mon Sep 17 00:00:00 2001 From: PartMan Date: Fri, 20 Mar 2026 19:27:37 +0530 Subject: [PATCH 1/2] chore: Stashset --- .eslintrc.json | 2 +- package-lock.json | 1145 ++++++++++++++++-- package.json | 10 +- src/ps/__tests__/games/chess.test.ts | 88 ++ src/ps/__tests__/games/connectfour.test.ts | 72 ++ src/ps/__tests__/games/lifecycle.test.ts | 235 ++++ src/ps/__tests__/games/othello.test.ts | 63 + src/ps/__tests__/games/snakesladders.test.ts | 81 ++ src/ps/__tests__/helpers/commands.ts | 67 + src/ps/__tests__/helpers/games.ts | 76 ++ src/ps/__tests__/mocks/client.ts | 42 + src/ps/__tests__/mocks/room.ts | 28 + src/ps/__tests__/mocks/user.ts | 25 + src/ps/__tests__/mocks/usersCore.ts | 76 ++ src/ps/__tests__/setup.ts | 7 + src/ps/games/battleship/index.ts | 82 +- src/ps/games/battleship/render.tsx | 35 +- src/ps/games/chess/index.ts | 29 +- src/ps/games/chess/render.tsx | 16 +- src/ps/games/connectfour/index.ts | 47 +- src/ps/games/connectfour/render.tsx | 14 +- src/ps/games/game.ts | 147 ++- src/ps/games/lightsout/index.ts | 22 +- src/ps/games/lightsout/render.tsx | 21 +- src/ps/games/mastermind/index.ts | 13 +- src/ps/games/mastermind/render.tsx | 31 +- src/ps/games/menus.tsx | 2 +- src/ps/games/othello/index.ts | 19 +- src/ps/games/othello/render.tsx | 16 +- src/ps/games/render.tsx | 23 +- src/ps/games/scrabble/index.ts | 24 +- src/ps/games/scrabble/render.tsx | 25 +- src/ps/games/snakesladders/index.ts | 43 +- src/ps/games/snakesladders/render.tsx | 16 +- src/ps/games/splendor/index.ts | 22 +- src/ps/games/splendor/render.tsx | 10 +- vitest.config.ts | 10 + 37 files changed, 2256 insertions(+), 428 deletions(-) create mode 100644 src/ps/__tests__/games/chess.test.ts create mode 100644 src/ps/__tests__/games/connectfour.test.ts create mode 100644 src/ps/__tests__/games/lifecycle.test.ts create mode 100644 src/ps/__tests__/games/othello.test.ts create mode 100644 src/ps/__tests__/games/snakesladders.test.ts create mode 100644 src/ps/__tests__/helpers/commands.ts create mode 100644 src/ps/__tests__/helpers/games.ts create mode 100644 src/ps/__tests__/mocks/client.ts create mode 100644 src/ps/__tests__/mocks/room.ts create mode 100644 src/ps/__tests__/mocks/user.ts create mode 100644 src/ps/__tests__/mocks/usersCore.ts create mode 100644 src/ps/__tests__/setup.ts create mode 100644 vitest.config.ts diff --git a/.eslintrc.json b/.eslintrc.json index 24493f5d..758f0501 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,7 +16,7 @@ "parserOptions": { "ecmaVersion": "latest" }, - "ignorePatterns": ["node_modules"], + "ignorePatterns": ["node_modules", "package.json", "package-lock.json"], "rules": { "@typescript-eslint/no-unused-vars": [ "error", diff --git a/package-lock.json b/package-lock.json index 0cbb9dfa..f5d3c87d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,12 @@ "eslint-plugin-import": "^2.31.0", "husky": "^8.0.3", "livereload": "^0.9.3", - "prettier": "^3.3.3" + "prettier": "^3.3.3", + "vite-tsconfig-paths": "^6.1.1", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=24" }, "optionalDependencies": { "@parcel/watcher-linux-x64-glibc": "^2.5.1", @@ -425,9 +430,10 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", @@ -510,6 +516,16 @@ "node": ">=12.4.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -704,9 +720,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", "cpu": [ "x64" ], @@ -803,6 +819,26 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher/node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", @@ -839,6 +875,268 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1004,9 +1302,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.3.tgz", - "integrity": "sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.1.tgz", + "integrity": "sha512-VJQxqrisHV+B394IgrOu8YsIIXZgffnf5tO+yc9Z/hoUpuZEvuQTjWwlnpZdpyD+0nx6LTD1/3k646JYm43yJA==", "cpu": [ "x64" ], @@ -1273,9 +1571,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", - "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", "cpu": [ "x64" ], @@ -1382,6 +1680,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -1454,6 +1763,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -1547,11 +1863,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.17.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", - "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/prop-types": { @@ -1850,6 +2167,119 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vladfrangu/async_event_emitter": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", @@ -2310,6 +2740,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2598,6 +3038,16 @@ } ] }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2746,6 +3196,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -3739,6 +4196,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3770,6 +4237,16 @@ "node": ">=0.8.x" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", @@ -4352,6 +4829,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, "node_modules/google-auth-library": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.1.0.tgz", @@ -5280,11 +5764,12 @@ } }, "node_modules/lightningcss": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", - "integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", "dependencies": { - "detect-libc": "^1.0.3" + "detect-libc": "^2.0.3" }, "engines": { "node": ">= 12.0.0" @@ -5294,25 +5779,47 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.1", - "lightningcss-darwin-x64": "1.29.1", - "lightningcss-freebsd-x64": "1.29.1", - "lightningcss-linux-arm-gnueabihf": "1.29.1", - "lightningcss-linux-arm64-gnu": "1.29.1", - "lightningcss-linux-arm64-musl": "1.29.1", - "lightningcss-linux-x64-gnu": "1.29.1", - "lightningcss-linux-x64-musl": "1.29.1", - "lightningcss-win32-arm64-msvc": "1.29.1", - "lightningcss-win32-x64-msvc": "1.29.1" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz", - "integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], + "license": "MPL-2.0", "optional": true, "os": [ "darwin" @@ -5326,9 +5833,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz", - "integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -5346,9 +5853,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz", - "integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -5366,9 +5873,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz", - "integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -5386,9 +5893,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz", - "integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -5406,9 +5913,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz", - "integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -5426,9 +5933,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -5446,9 +5953,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz", - "integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -5466,9 +5973,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz", - "integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -5486,9 +5993,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz", - "integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -5505,24 +6012,13 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss/node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz", - "integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=8" } }, "node_modules/livereload": { @@ -5643,6 +6139,16 @@ "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -5847,14 +6353,33 @@ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "engines": { - "node": ">=4" + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6008,6 +6533,20 @@ "node": ">=18.0.0" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6156,6 +6695,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6240,6 +6786,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6650,6 +7225,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" + } + }, "node_modules/router": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", @@ -7004,6 +7613,13 @@ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -7030,6 +7646,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -7047,6 +7673,13 @@ "memory-pager": "^1.0.2" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -7055,6 +7688,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -7242,6 +7882,81 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7358,6 +8073,28 @@ } } }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "deprecated": "unmaintained", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -7524,9 +8261,10 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", @@ -7632,6 +8370,222 @@ "d3-timer": "^3.0.1" } }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", + "integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", @@ -7921,6 +8875,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", diff --git a/package.json b/package.json index e12706bb..d76c351b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "prestart": "bash ./scripts/prestart.sh", "start": "ts-node --swc src/index.ts", "tailwind": "npx @tailwindcss/cli -i ./src/web/styles/tailwind.css -o ./src/web/react/compiled/styles.css", - "test": "npm run lint && npm run tsc", + "test:staged": "npm run lint && npm run tsc", + "test": "npm run test:staged && vitest run", "tsc": "tsc", "webpack": "webpack -c src/web/webpack/webpack.config.ts" }, @@ -64,12 +65,17 @@ "eslint-plugin-import": "^2.31.0", "husky": "^8.0.3", "livereload": "^0.9.3", - "prettier": "^3.3.3" + "prettier": "^3.3.3", + "vite-tsconfig-paths": "^6.1.1", + "vitest": "^4.0.18" }, "optionalDependencies": { "@parcel/watcher-linux-x64-glibc": "^2.5.1", "@swc/core-linux-x64-gnu": "^1.2.160", "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", "lightningcss-linux-x64-gnu": "^1.29.1" + }, + "engines": { + "node": ">=24" } } diff --git a/src/ps/__tests__/games/chess.test.ts b/src/ps/__tests__/games/chess.test.ts new file mode 100644 index 00000000..fb547c35 --- /dev/null +++ b/src/ps/__tests__/games/chess.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('@/cache/games', () => ({ + gameCache: { set: vi.fn(), delete: vi.fn(), get: vi.fn(), getByGame: vi.fn(), clearOldBackups: vi.fn() }, +})); + +import { runGamePlayCommand } from '@/ps/__tests__/helpers/commands'; +import { createGame, getButtonActions } from '@/ps/__tests__/helpers/games'; +import { client } from '@/ps/__tests__/mocks/client'; +import { mockRoom } from '@/ps/__tests__/mocks/room'; +import { mockUser } from '@/ps/__tests__/mocks/user'; +import { Chess } from '@/ps/games/chess'; +import { meta } from '@/ps/games/chess/meta'; + +import type { MockRoom } from '@/ps/__tests__/mocks/room'; +import type { MockUser } from '@/ps/__tests__/mocks/user'; +import type { Square } from 'chess.js'; + +function setupGame() { + const room = mockRoom('boardgames', client); + const game = createGame(Chess, meta, room, mockUser('Tester')); + const userW = mockUser('White'); + const userB = mockUser('Black'); + game.addPlayer(userW, 'W'); + game.addPlayer(userB, 'B'); // auto-starts + return { game, room, userW, userB }; +} + +/** + * Selects a piece and makes a move, verifying the required buttons are present + * in the rendered HTML at each step. + */ +async function selectAndMove( + game: InstanceType, + room: MockRoom, + user: MockUser, + turn: 'W' | 'B', + from: Square, + san: string +): Promise { + const beforeSelect = getButtonActions(game.render(turn), game); + expect(beforeSelect, `Expected "select ${from}" to be available for ${turn}`).toContain(`select ${from}`); + await runGamePlayCommand(game, user, room, `select ${from}`); + + const afterSelect = getButtonActions(game.render(turn), game); + expect(afterSelect, `Expected "move ${san}" to be available after selecting ${from}`).toContain(`move ${san}`); + await runGamePlayCommand(game, user, room, `move ${san}`); +} + +describe("Chess playthrough — Scholar's Mate", () => { + /** + * Scholar's Mate: White wins on move 4. + * 1. e4 e5 + * 2. Bc4 Nc6 + * 3. Qh5 Nf6?? + * 4. Qxf7# + */ + it("plays Scholar's Mate using only HTML button actions", async () => { + const { game, room, userW, userB } = setupGame(); + + await selectAndMove(game, room, userW, 'W', 'e2', 'e4'); + await selectAndMove(game, room, userB, 'B', 'e7', 'e5'); + await selectAndMove(game, room, userW, 'W', 'f1', 'Bc4'); + await selectAndMove(game, room, userB, 'B', 'b8', 'Nc6'); + await selectAndMove(game, room, userW, 'W', 'd1', 'Qh5'); + await selectAndMove(game, room, userB, 'B', 'g8', 'Nf6'); + await selectAndMove(game, room, userW, 'W', 'h5', 'Qxf7#'); + + expect(game.winCtx).toBeDefined(); + expect(game.winCtx?.type).toBe('win'); + }); + + it('renders the final state after checkmate', async () => { + const { game, room, userW, userB } = setupGame(); + + await selectAndMove(game, room, userW, 'W', 'e2', 'e4'); + await selectAndMove(game, room, userB, 'B', 'e7', 'e5'); + await selectAndMove(game, room, userW, 'W', 'f1', 'Bc4'); + await selectAndMove(game, room, userB, 'B', 'b8', 'Nc6'); + await selectAndMove(game, room, userW, 'W', 'd1', 'Qh5'); + await selectAndMove(game, room, userB, 'B', 'g8', 'Nf6'); + await selectAndMove(game, room, userW, 'W', 'h5', 'Qxf7#'); + + expect(() => game.render('W')).not.toThrow(); + expect(() => game.render('B')).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); +}); diff --git a/src/ps/__tests__/games/connectfour.test.ts b/src/ps/__tests__/games/connectfour.test.ts new file mode 100644 index 00000000..fbe25590 --- /dev/null +++ b/src/ps/__tests__/games/connectfour.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('@/cache/games', () => ({ + gameCache: { set: vi.fn(), delete: vi.fn(), get: vi.fn(), getByGame: vi.fn(), clearOldBackups: vi.fn() }, +})); + +import { runGamePlayCommand } from '@/ps/__tests__/helpers/commands'; +import { createGame, getButtonActions } from '@/ps/__tests__/helpers/games'; +import { client } from '@/ps/__tests__/mocks/client'; +import { mockRoom } from '@/ps/__tests__/mocks/room'; +import { mockUser } from '@/ps/__tests__/mocks/user'; +import { ConnectFour } from '@/ps/games/connectfour'; +import { meta } from '@/ps/games/connectfour/meta'; + +function setupGame() { + const room = mockRoom('boardgames', client); + const game = createGame(ConnectFour, meta, room, mockUser('Alice')); + const userY = mockUser('Yellow'); + const userR = mockUser('Red'); + game.addPlayer(userY, 'Y'); + game.addPlayer(userR, 'R'); // auto-starts + return { game, room, userY, userR }; +} + +describe('ConnectFour playthrough', () => { + /** + * Yellow wins horizontally at the bottom row: + * Y col 0, R col 4, Y col 1, R col 5, Y col 2, R col 6, Y col 3 → Y wins + */ + it('plays a full game using only HTML button actions', async () => { + const { game, room, userY, userR } = setupGame(); + + const moves: Array<{ turn: 'Y' | 'R'; user: ReturnType; col: string }> = [ + { turn: 'Y', user: userY, col: '0' }, + { turn: 'R', user: userR, col: '4' }, + { turn: 'Y', user: userY, col: '1' }, + { turn: 'R', user: userR, col: '5' }, + { turn: 'Y', user: userY, col: '2' }, + { turn: 'R', user: userR, col: '6' }, + { turn: 'Y', user: userY, col: '3' }, + ]; + + for (const { turn, user, col } of moves) { + expect(game.winCtx, `Expected no winner yet before move on col ${col}`).toBeUndefined(); + expect(game.turn).toBe(turn); + + const actions = getButtonActions(game.render(turn), game); + expect(actions, `Expected col ${col} to be a valid action for ${turn}`).toContain(col); + + await runGamePlayCommand(game, user, room, col); + } + + expect(game.winCtx).toBeDefined(); + expect(game.winCtx?.type).toBe('win'); + }); + + it('renders the final state after game ends', async () => { + const { game, room, userY, userR } = setupGame(); + await runGamePlayCommand(game, userY, room, '0'); + await runGamePlayCommand(game, userR, room, '4'); + await runGamePlayCommand(game, userY, room, '1'); + await runGamePlayCommand(game, userR, room, '5'); + await runGamePlayCommand(game, userY, room, '2'); + await runGamePlayCommand(game, userR, room, '6'); + await runGamePlayCommand(game, userY, room, '3'); + + // After the game ends, both perspectives must still render without errors + expect(() => game.render('Y')).not.toThrow(); + expect(() => game.render('R')).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); +}); diff --git a/src/ps/__tests__/games/lifecycle.test.ts b/src/ps/__tests__/games/lifecycle.test.ts new file mode 100644 index 00000000..975f4c19 --- /dev/null +++ b/src/ps/__tests__/games/lifecycle.test.ts @@ -0,0 +1,235 @@ +import { describe, expect, it, vi } from 'vitest'; + +// Prevent FlatCache from writing game backups to disk during tests. +vi.mock('@/cache/games', () => ({ + gameCache: { set: vi.fn(), delete: vi.fn(), get: vi.fn(), getByGame: vi.fn(), clearOldBackups: vi.fn() }, +})); + +import { createGame, mockT } from '@/ps/__tests__/helpers/games'; +import { client } from '@/ps/__tests__/mocks/client'; +import { mockRoom } from '@/ps/__tests__/mocks/room'; +import { mockUser } from '@/ps/__tests__/mocks/user'; +import { Battleship } from '@/ps/games/battleship'; +import { meta as battleshipMeta } from '@/ps/games/battleship/meta'; +import { Chess } from '@/ps/games/chess'; +import { meta as chessMeta } from '@/ps/games/chess/meta'; +import { ConnectFour } from '@/ps/games/connectfour'; +import { meta as connectfourMeta } from '@/ps/games/connectfour/meta'; +import { LightsOut } from '@/ps/games/lightsout'; +import { meta as lightsoutMeta } from '@/ps/games/lightsout/meta'; +import { Mastermind } from '@/ps/games/mastermind'; +import { meta as mastermindMeta } from '@/ps/games/mastermind/meta'; +import { Othello } from '@/ps/games/othello'; +import { meta as othelloMeta } from '@/ps/games/othello/meta'; +import { Scrabble } from '@/ps/games/scrabble'; +import { meta as scrabbleMeta } from '@/ps/games/scrabble/meta'; +import { SnakesLadders } from '@/ps/games/snakesladders'; +import { meta as snakesladdersMeta } from '@/ps/games/snakesladders/meta'; +import { Splendor } from '@/ps/games/splendor'; +import { meta as splendorMeta } from '@/ps/games/splendor/meta'; + +function setup(name: string) { + const room = mockRoom('boardgames', client); + const creator = mockUser(name); + return { room, creator }; +} + +describe('Chess lifecycle', () => { + it('creates a game without errors', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Chess, chessMeta, room, creator); + expect(game.id).toBe('#TEST'); + expect(game.started).toBe(false); + }); + + it('accepts players on both sides and auto-starts when full', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Chess, chessMeta, room, creator); + const alice = mockUser('Alice'); + const bob = mockUser('Bob'); + + const resA = game.addPlayer(alice, 'W'); + expect(resA.success).toBe(true); + expect(game.started).toBe(false); + + // Sided 2-player games auto-start when the last slot is filled. + const resB = game.addPlayer(bob, 'B'); + expect(resB.success).toBe(true); + expect(game.started).toBe(true); + expect(Object.keys(game.players)).toHaveLength(2); + }); + + it('rejects a duplicate player', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Chess, chessMeta, room, creator); + const alice = mockUser('Alice'); + game.addPlayer(alice, 'W'); + expect(() => game.addPlayer(alice, 'B')).toThrow(); + }); + + it('rejects a player on an occupied side', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Chess, chessMeta, room, creator); + const alice = mockUser('Alice'); + const bob = mockUser('Bob'); + game.addPlayer(alice, 'W'); + const res = game.addPlayer(bob, 'W'); + expect(res.success).toBe(false); + }); + + it('is startable when one slot remains', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Chess, chessMeta, room, creator); + const alice = mockUser('Alice'); + game.addPlayer(alice, 'W'); + // With one player added, the remaining slot makes it startable. + // But for sided games, startable() requires ALL turns to have players. + expect(game.startable()).toBe(false); + }); + + it('allows a player to leave before the game starts', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Chess, chessMeta, room, creator); + const alice = mockUser('Alice'); + game.addPlayer(alice, 'W'); + const res = game.removePlayer(alice); + expect(res.success).toBe(true); + expect(Object.keys(game.players)).toHaveLength(0); + }); + + it('starts with White to move and renders without throwing', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Chess, chessMeta, room, creator); + game.addPlayer(mockUser('Alice'), 'W'); + game.addPlayer(mockUser('Bob'), 'B'); // auto-starts + expect(game.started).toBe(true); + expect(game.turn).toBe('W'); + expect(() => game.render('W')).not.toThrow(); + expect(() => game.render('B')).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); +}); + +describe('ConnectFour lifecycle', () => { + it('auto-starts when both sides are filled and renders without errors', () => { + const { room, creator } = setup('Alice'); + const game = createGame(ConnectFour, connectfourMeta, room, creator); + game.addPlayer(mockUser('Alice'), 'Y'); + game.addPlayer(mockUser('Bob'), 'R'); // auto-starts + expect(game.started).toBe(true); + expect(game.turn).toBe('Y'); + expect(() => game.render('Y')).not.toThrow(); + expect(() => game.render('R')).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); + + it('allows random side assignment', () => { + const { room, creator } = setup('Alice'); + const game = createGame(ConnectFour, connectfourMeta, room, creator); + const res = game.addPlayer(mockUser('Alice'), '-'); + expect(res.success).toBe(true); + if (res.success) expect(['Y', 'R']).toContain(res.data.as); + }); +}); + +describe('Othello lifecycle', () => { + it('auto-starts when both sides are filled and renders without errors', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Othello, othelloMeta, room, creator); + game.addPlayer(mockUser('Alice'), 'W'); + game.addPlayer(mockUser('Bob'), 'B'); // auto-starts + expect(game.started).toBe(true); + expect(() => game.render('W')).not.toThrow(); + expect(() => game.render('B')).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); +}); + +describe('Battleship lifecycle', () => { + it('auto-starts when both sides are filled and renders without errors', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Battleship, battleshipMeta, room, creator); + game.addPlayer(mockUser('Alice'), 'A'); + game.addPlayer(mockUser('Bob'), 'B'); // auto-starts + expect(game.started).toBe(true); + expect(() => game.render('A')).not.toThrow(); + expect(() => game.render('B')).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); +}); + +describe('SnakesLadders lifecycle', () => { + it('requires at least 2 players to be startable', () => { + const { room, creator } = setup('Alice'); + const game = createGame(SnakesLadders, snakesladdersMeta, room, creator); + game.addPlayer(mockUser('Alice'), null); + expect(game.startable()).toBe(false); + game.addPlayer(mockUser('Bob'), null); + expect(game.startable()).toBe(true); + }); + + it('starts and renders without errors', () => { + const { room, creator } = setup('Alice'); + const game = createGame(SnakesLadders, snakesladdersMeta, room, creator); + game.addPlayer(mockUser('Alice'), null); + game.addPlayer(mockUser('Bob'), null); + game.start(); + expect(game.started).toBe(true); + expect(game.turn).toBeTruthy(); + expect(() => game.render(game.turn)).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); +}); + +describe('Scrabble lifecycle', () => { + // Scrabble has autostart: false and maxSize: 4, so explicit start is required. + it('requires explicit start when under max capacity, then renders without errors', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Scrabble, scrabbleMeta, room, creator); + game.addPlayer(mockUser('Alice'), null); + game.addPlayer(mockUser('Bob'), null); + expect(game.startable()).toBe(true); + expect(game.started).toBe(false); + game.start(); + expect(game.started).toBe(true); + expect(() => game.render(game.turn)).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); +}); + +describe('Splendor lifecycle', () => { + it('requires explicit start when under max capacity, then renders without errors', () => { + const { room, creator } = setup('Alice'); + const game = createGame(Splendor, splendorMeta, room, creator); + game.addPlayer(mockUser('Alice'), null); + game.addPlayer(mockUser('Bob'), null); + expect(game.startable()).toBe(true); + expect(game.started).toBe(false); + game.start(); + expect(game.started).toBe(true); + expect(() => game.render(game.turn)).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); +}); + +describe('LightsOut lifecycle', () => { + it('creates and renders without errors', () => { + const { room, creator } = setup('Alice'); + const alice = mockUser('Alice'); + const game = createGame(LightsOut, lightsoutMeta, room, creator); + game.after({ id: game.id, meta: lightsoutMeta, room: room, $T: mockT, args: [], by: alice }); + expect(game.started).toBe(true); + expect(() => game.render(alice.id)).not.toThrow(); + }); +}); + +describe('Mastermind lifecycle', () => { + it('creates and renders without errors', () => { + const { room, creator } = setup('Alice'); + const alice = mockUser('Alice'); + const game = createGame(Mastermind, mastermindMeta, room, creator); + game.after({ id: game.id, meta: mastermindMeta, room: room, $T: mockT, args: [], by: alice }); + expect(game.started).toBe(true); + expect(() => game.render(alice.id)).not.toThrow(); + }); +}); diff --git a/src/ps/__tests__/games/othello.test.ts b/src/ps/__tests__/games/othello.test.ts new file mode 100644 index 00000000..be6ea294 --- /dev/null +++ b/src/ps/__tests__/games/othello.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('@/cache/games', () => ({ + gameCache: { set: vi.fn(), delete: vi.fn(), get: vi.fn(), getByGame: vi.fn(), clearOldBackups: vi.fn() }, +})); + +import { runGamePlayCommand } from '@/ps/__tests__/helpers/commands'; +import { createGame, getButtonActions } from '@/ps/__tests__/helpers/games'; +import { client } from '@/ps/__tests__/mocks/client'; +import { mockRoom } from '@/ps/__tests__/mocks/room'; +import { mockUser } from '@/ps/__tests__/mocks/user'; +import { Othello } from '@/ps/games/othello'; +import { meta } from '@/ps/games/othello/meta'; + +function setupGame() { + const room = mockRoom('boardgames', client); + const game = createGame(Othello, meta, room, mockUser('Alice')); + const userW = mockUser('White'); + const userB = mockUser('Black'); + game.addPlayer(userW, 'W'); + game.addPlayer(userB, 'B'); // auto-starts + return { game, room, userW, userB, users: { W: userW, B: userB } }; +} + +describe('Othello playthrough', () => { + it('plays a full game using only HTML button actions', async () => { + const { game, room, users } = setupGame(); + + let safeguard = 0; + const MAX_MOVES = 64; + + while (!game.winCtx && safeguard++ < MAX_MOVES) { + const turn = game.turn as 'W' | 'B'; + const user = users[turn]; + + const actions = getButtonActions(game.render(turn), game); + expect(actions.length, `Expected at least one valid action for ${turn}`).toBeGreaterThan(0); + + // Play the first available valid move + const [move] = actions; + await runGamePlayCommand(game, user, room, move); + } + + expect(safeguard, 'Game should end within the maximum number of moves').toBeLessThanOrEqual(MAX_MOVES); + expect(game.winCtx).toBeDefined(); + }); + + it('renders the final state after game ends', async () => { + const { game, room, userW, userB } = setupGame(); + + let safeguard = 0; + while (!game.winCtx && safeguard++ < 64) { + const turn = game.turn as 'W' | 'B'; + const user = turn === 'W' ? userW : userB; + const [action] = getButtonActions(game.render(turn), game); + await runGamePlayCommand(game, user, room, action); + } + + expect(() => game.render('W')).not.toThrow(); + expect(() => game.render('B')).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); +}); diff --git a/src/ps/__tests__/games/snakesladders.test.ts b/src/ps/__tests__/games/snakesladders.test.ts new file mode 100644 index 00000000..c053d940 --- /dev/null +++ b/src/ps/__tests__/games/snakesladders.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('@/cache/games', () => ({ + gameCache: { set: vi.fn(), delete: vi.fn(), get: vi.fn(), getByGame: vi.fn(), clearOldBackups: vi.fn() }, +})); + +import { runGamePlayCommand } from '@/ps/__tests__/helpers/commands'; +import { createGame, getButtonActions } from '@/ps/__tests__/helpers/games'; +import { client } from '@/ps/__tests__/mocks/client'; +import { mockRoom } from '@/ps/__tests__/mocks/room'; +import { mockUser } from '@/ps/__tests__/mocks/user'; +import { SnakesLadders } from '@/ps/games/snakesladders'; +import { meta } from '@/ps/games/snakesladders/meta'; +import { useRNG } from '@/utils/random'; + +function setupGame(seed?: number) { + const room = mockRoom('boardgames', client); + const game = createGame(SnakesLadders, meta, room, mockUser('Tester')); + const userA = mockUser('PlayerA'); + const userB = mockUser('PlayerB'); + game.addPlayer(userA, null); + game.addPlayer(userB, null); + + if (seed !== undefined) { + game.seed = seed; + const prng = useRNG(seed); + game.prng = () => { + game.prngCalls++; + return prng(); + }; + } + + game.start(); + return { + game, + room, + users: new Map>([ + [userA.id, userA], + [userB.id, userB], + ]), + }; +} + +describe('SnakesLadders playthrough', () => { + it('plays a full game using only the roll button', async () => { + const { game, room, users } = setupGame(42); + + // A Snakes & Ladders game with 2 players terminates in well under 500 rolls. + const MAX_ROLLS = 500; + let rolls = 0; + + while (!game.winCtx && rolls++ < MAX_ROLLS) { + const turn = game.turn!; + const user = users.get(turn)!; + expect(user, `No user found for turn "${turn}"`).toBeDefined(); + + // The roll button has an empty game ctx (just the "!" play alias). + const actions = getButtonActions(game.render(turn), game); + expect(actions, 'Expected the roll button to be present').toContain(''); + + await runGamePlayCommand(game, user, room, ''); + } + + expect(rolls, 'Game should end within the maximum number of rolls').toBeLessThanOrEqual(MAX_ROLLS); + expect(game.winCtx).toBeDefined(); + expect(game.winCtx?.type).toBe('win'); + }); + + it('renders the final state after game ends', async () => { + const { game, room, users } = setupGame(99); + + let rolls = 0; + while (!game.winCtx && rolls++ < 500) { + const user = users.get(game.turn!)!; + await runGamePlayCommand(game, user, room, ''); + } + + expect(() => game.render(game.turn)).not.toThrow(); + expect(() => game.render(null)).not.toThrow(); + }); +}); diff --git a/src/ps/__tests__/helpers/commands.ts b/src/ps/__tests__/helpers/commands.ts new file mode 100644 index 00000000..9d3fc95f --- /dev/null +++ b/src/ps/__tests__/helpers/commands.ts @@ -0,0 +1,67 @@ +import { mockT } from '@/ps/__tests__/helpers/games'; +import { command as gameTopLevelCommands } from '@/ps/commands/games/core'; + +import type { PSMessageTranslated } from '@/i18n/types'; +import type { MockRoom } from '@/ps/__tests__/mocks/room'; +import type { MockUser } from '@/ps/__tests__/mocks/user'; +import type { CommonGame } from '@/ps/games/game'; +import type { PSCommandContext } from '@/types/chat'; +import type { PSMessage } from '@/types/ps'; + +export type { mockT }; + +/** + * Builds a minimal PSCommandContext for unit-testing individual PS commands. + */ +export function buildCommandContext(user: MockUser, room: MockRoom, overrides: Partial = {}): PSCommandContext { + const message = { + author: user, + target: room, + type: 'chat', + reply: () => null, + privateReply: () => null, + replyHTML: () => null, + sendHTML: () => null, + } as unknown as PSMessageTranslated; + + return { + $T: mockT, + args: [], + arg: '', + rawArgs: [], + command: [], + originalCommand: [], + message: message as unknown as PSMessage, + run: async () => undefined, + unsafeRun: async () => undefined, + broadcast: () => null, + broadcastHTML: () => null, + checkPermissions: () => false, + hasFeature: () => null, + ...overrides, + } as unknown as PSCommandContext; +} + +/** + * Runs a PSCommand's `run()` method with a pre-built context. + */ +export async function runCommand( + run: (ctx: PSCommandContext & { message: PSMessage }) => Promise, + user: MockUser, + room: MockRoom, + overrides: Partial = {} +): Promise { + const ctx = buildCommandContext(user, room, overrides); + await run(ctx as PSCommandContext & { message: PSMessage }); +} + +/** + * Performs a move the same way chat does: the game module's `play` handler with + * `arg` shaped like `[id], [move]` (e.g. `,connectfour play #TEST, 3`). + */ +export async function runGamePlayCommand(game: CommonGame, user: MockUser, room: MockRoom, moveCtx: string): Promise { + const playRun = gameTopLevelCommands.find(c => c.name === game.meta.id)?.children?.play?.run; + if (!playRun) throw new Error(`No play subcommand for game ${game.meta.id}`); + const arg = moveCtx ? `${game.id}, ${moveCtx}` : `${game.id},`; + await runCommand(playRun, user, room, { arg }); +} diff --git a/src/ps/__tests__/helpers/games.ts b/src/ps/__tests__/helpers/games.ts new file mode 100644 index 00000000..e175bbcf --- /dev/null +++ b/src/ps/__tests__/helpers/games.ts @@ -0,0 +1,76 @@ +import { jsxToHTML } from '@/utils/jsxToHTML'; + +import type { BaseLookup, TranslationFn } from '@/i18n/types'; +import type { MockRoom } from '@/ps/__tests__/mocks/room'; +import type { MockUser } from '@/ps/__tests__/mocks/user'; +import type { BaseContext, CommonGame } from '@/ps/games/game'; +import type { Meta } from '@/ps/games/types'; +import type { ReactElement } from 'react'; + +/** + * A mock translation function that returns a deterministic string for each key and variable set. + * Satisfies the TranslationFn contract by casting. + */ +export const mockT = ((key: Lookup, vars?: Record) => + vars ? `${key}:${JSON.stringify(vars)}` : key) as unknown as TranslationFn; + +export function createGame( + GameClass: { new (ctx: BaseContext): G }, + gameMeta: Meta, + room: MockRoom, + creator: MockUser, + opts: { id?: string; args?: string[] } = {} +): G { + return new GameClass({ + id: opts.id ?? '#TEST', + meta: gameMeta, + room, + $T: mockT, + args: opts.args ?? [], + by: creator, + }); +} + +/** Subcommand aliases for the "play" action used in game button values. */ +const PLAY_SUBCOMMANDS = new Set(['play', 'p', '!']); + +/** + * Parses a rendered game page HTML and returns the game-specific action strings + * from all "play" action buttons — the move portion after the game id in + * `, play [id], [move]` (see `runGamePlayCommand`). + * + * A play button value has the shape: + * `${game.msg} ${playAlias} ${gameCtx}` + * e.g. `/msgroom boardgames,/botmsg partbot,,@#TEST ! select e2` + * + * The returned action strings are just the `gameCtx` part, e.g. `select e2`. + */ +export function getButtonActions(element: ReactElement, game: CommonGame): string[] { + const html = jsxToHTML(element); + const msgPrefix = game.msg + ' '; + const actions: string[] = []; + + // Match all