From d7ddbab52c9dee6d870f71dfd37aa0ce3ba2d0a0 Mon Sep 17 00:00:00 2001 From: julien Lengrand-Lambert Date: Mon, 12 Jan 2026 23:14:12 +0100 Subject: [PATCH] Adding Bluesky to the list of social networks on the client side (#186) * MAESTRO: Add Bluesky social network support to leaderboard registration - Add blueskyHandle field to LeaderboardRegistration interface - Create BlueskySkyIcon component with official butterfly logo - Add Bluesky input field to registration modal - Update IPC handlers to accept and transmit blueskyHandle - Support both username.bsky.social and custom domain formats - Strip @ prefix from handles for consistency with other social fields - Update main process to include blueskyHandle in API submission Co-Authored-By: Claude Sonnet 4.5 * MAESTRO: Add comprehensive test suite for Bluesky leaderboard integration - Created LeaderboardRegistrationModal.test.tsx with 20 tests - Tests cover Bluesky field rendering, @ prefix stripping, state persistence, and theme styling - 16/20 tests passing (4 form submission tests have timing issues in test environment) - Updated test setup to include leaderboard API mock - All existing tests remain passing (9,893/9,898 pass) Co-Authored-By: Claude Sonnet 4.5 * fix: add blueskyHandle to MaestroAPI type and fix test setup - Added blueskyHandle to global.d.ts MaestroAPI interface (was missing) - Added missing leaderboard mock methods to test setup - Fixed form submission tests to use existing registration --------- Co-authored-by: Claude Sonnet 4.5 Co-authored-by: Pedram Amini --- package-lock.json | 80 ++- .../LeaderboardRegistrationModal.test.tsx | 571 ++++++++++++++++++ src/__tests__/setup.ts | 7 + src/main/index.ts | 2 + src/main/preload.ts | 2 + .../LeaderboardRegistrationModal.tsx | 27 + src/renderer/global.d.ts | 1 + src/renderer/types/index.ts | 1 + 8 files changed, 645 insertions(+), 46 deletions(-) create mode 100644 src/__tests__/renderer/components/LeaderboardRegistrationModal.test.tsx diff --git a/package-lock.json b/package-lock.json index 562b4c1d..75efa3bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "maestro", - "version": "0.14.0", + "version": "0.14.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "maestro", - "version": "0.14.0", + "version": "0.14.5", "hasInstallScript": true, "license": "AGPL 3.0", "dependencies": { @@ -257,7 +257,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -661,7 +660,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -705,7 +703,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2279,7 +2276,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -2301,7 +2297,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz", "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==", "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -2314,7 +2309,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2330,7 +2324,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz", "integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", @@ -2718,7 +2711,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2735,7 +2727,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", @@ -2753,7 +2744,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -3644,7 +3634,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4175,7 +4166,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4187,7 +4177,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4313,7 +4302,6 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -4744,7 +4732,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4826,7 +4813,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5830,7 +5816,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -6313,7 +6298,6 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -7039,7 +7023,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -7449,7 +7432,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -7947,7 +7929,6 @@ "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "builder-util": "24.13.1", @@ -8043,7 +8024,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dompurify": { "version": "3.3.0", @@ -8187,6 +8169,7 @@ "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "archiver": "^5.3.1", @@ -8200,6 +8183,7 @@ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", @@ -8219,6 +8203,7 @@ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", @@ -8241,6 +8226,7 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -8257,6 +8243,7 @@ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", @@ -8273,6 +8260,7 @@ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" @@ -8287,6 +8275,7 @@ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -8302,6 +8291,7 @@ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -8314,7 +8304,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/electron-builder-squirrel-windows/node_modules/string_decoder": { "version": "1.1.1", @@ -8322,6 +8313,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -8332,6 +8324,7 @@ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -8342,6 +8335,7 @@ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", @@ -8357,6 +8351,7 @@ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", @@ -9028,7 +9023,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10909,7 +10903,6 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -11730,7 +11723,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12144,14 +12136,16 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", @@ -12164,7 +12158,8 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.isequal": { "version": "4.5.0", @@ -12178,7 +12173,8 @@ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -12192,7 +12188,8 @@ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -12283,6 +12280,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -14773,7 +14771,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14998,6 +14995,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -15013,6 +15011,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -15357,7 +15356,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -15387,7 +15385,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -15435,7 +15432,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -15622,8 +15618,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -17373,7 +17368,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -17684,7 +17678,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18033,7 +18026,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -18539,7 +18531,6 @@ "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.15", "@vitest/mocker": "4.0.15", @@ -19130,7 +19121,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -19144,7 +19134,6 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -19735,7 +19724,6 @@ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/__tests__/renderer/components/LeaderboardRegistrationModal.test.tsx b/src/__tests__/renderer/components/LeaderboardRegistrationModal.test.tsx new file mode 100644 index 00000000..2328b646 --- /dev/null +++ b/src/__tests__/renderer/components/LeaderboardRegistrationModal.test.tsx @@ -0,0 +1,571 @@ +/** + * @fileoverview Tests for LeaderboardRegistrationModal component + * Tests: Bluesky field rendering, @ prefix stripping, form submission, state persistence + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { LeaderboardRegistrationModal } from '../../../renderer/components/LeaderboardRegistrationModal'; +import type { Theme, AutoRunStats, LeaderboardRegistration } from '../../../renderer/types'; +import type { KeyboardMasteryStats } from '../../../shared/types'; + +// Mock layer stack context +const mockRegisterLayer = vi.fn(() => 'layer-leaderboard-123'); +const mockUnregisterLayer = vi.fn(); + +vi.mock('../../../renderer/contexts/LayerStackContext', () => ({ + useLayerStack: () => ({ + registerLayer: mockRegisterLayer, + unregisterLayer: mockUnregisterLayer, + }), +})); + +// Add __APP_VERSION__ global +(globalThis as unknown as { __APP_VERSION__: string }).__APP_VERSION__ = '1.0.0'; + +// Create test theme +const createTheme = (): Theme => ({ + id: 'test-dark', + name: 'Test Dark', + mode: 'dark', + colors: { + bgMain: '#1a1a2e', + bgSidebar: '#16213e', + bgActivity: '#0f3460', + textMain: '#e8e8e8', + textDim: '#888888', + accent: '#7b2cbf', + border: '#333355', + success: '#22c55e', + warning: '#f59e0b', + error: '#ef4444', + info: '#3b82f6', + bgAccentHover: '#9333ea', + }, +}); + +// Create test autoRunStats +const createAutoRunStats = (overrides: Partial = {}): AutoRunStats => ({ + cumulativeTimeMs: 120000, // 2 minutes + longestRunMs: 60000, // 1 minute + totalRuns: 5, + lastBadgeAcknowledged: null, + badgeHistory: [], + ...overrides, +}); + +// Create test keyboard mastery stats +const createKeyboardMasteryStats = (overrides: Partial = {}): KeyboardMasteryStats => ({ + shortcutUsageCounts: {}, + totalShortcutsUsed: 50, + firstShortcutAt: new Date('2024-01-01').toISOString(), + lastShortcutAt: new Date('2024-01-10').toISOString(), + usedShortcuts: ['openCommandPalette', 'newSession', 'closeSession'], + currentLevel: 1, + ...overrides, +}); + +describe('LeaderboardRegistrationModal', () => { + let theme: Theme; + let autoRunStats: AutoRunStats; + let keyboardMasteryStats: KeyboardMasteryStats; + let onClose: ReturnType; + let onSave: ReturnType; + + beforeEach(() => { + theme = createTheme(); + autoRunStats = createAutoRunStats(); + keyboardMasteryStats = createKeyboardMasteryStats(); + onClose = vi.fn(); + onSave = vi.fn(); + + // Mock leaderboard API + vi.mocked(window.maestro.leaderboard.submit).mockResolvedValue({ + success: true, + rank: 42, + }); + + // Reset layer stack mocks + mockRegisterLayer.mockClear().mockReturnValue('layer-leaderboard-123'); + mockUnregisterLayer.mockClear(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('Bluesky field rendering', () => { + it('should render Bluesky input field', () => { + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social'); + expect(blueskyInput).toBeInTheDocument(); + }); + + it('should render Bluesky icon with correct styling', () => { + render( + + ); + + // The BlueskySkyIcon renders an SVG path - check for the icon container + const blueskyInput = screen.getByPlaceholderText('username.bsky.social'); + const iconContainer = blueskyInput.parentElement?.querySelector('svg'); + expect(iconContainer).toBeInTheDocument(); + expect(iconContainer).toHaveClass('w-4', 'h-4'); + }); + + it('should have correct placeholder text', () => { + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social'); + expect(blueskyInput).toHaveAttribute('placeholder', 'username.bsky.social'); + }); + }); + + describe('@ prefix stripping', () => { + it('should strip leading @ when user types it', () => { + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social') as HTMLInputElement; + fireEvent.change(blueskyInput, { target: { value: '@username.bsky.social' } }); + + expect(blueskyInput.value).toBe('username.bsky.social'); + }); + + it('should handle multiple @ symbols (only strip the leading one)', () => { + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social') as HTMLInputElement; + fireEvent.change(blueskyInput, { target: { value: '@user@name.bsky.social' } }); + + expect(blueskyInput.value).toBe('user@name.bsky.social'); + }); + + it('should allow input without @ prefix', () => { + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social') as HTMLInputElement; + fireEvent.change(blueskyInput, { target: { value: 'username.bsky.social' } }); + + expect(blueskyInput.value).toBe('username.bsky.social'); + }); + }); + + describe('Custom domain support', () => { + it('should accept custom domain handles', () => { + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social') as HTMLInputElement; + fireEvent.change(blueskyInput, { target: { value: 'user.example.com' } }); + + expect(blueskyInput.value).toBe('user.example.com'); + }); + + it('should strip @ from custom domain handles', () => { + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social') as HTMLInputElement; + fireEvent.change(blueskyInput, { target: { value: '@user.example.com' } }); + + expect(blueskyInput.value).toBe('user.example.com'); + }); + }); + + describe('State persistence', () => { + it('should load existing Bluesky handle from registration', () => { + const existingRegistration: LeaderboardRegistration = { + displayName: 'Test User', + gitHubUsername: 'testuser', + twitterHandle: 'testuser', + discordUsername: 'testuser#1234', + blueskyHandle: 'testuser.bsky.social', + submittedAt: new Date().toISOString(), + }; + + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social') as HTMLInputElement; + expect(blueskyInput.value).toBe('testuser.bsky.social'); + }); + + it('should load custom domain Bluesky handle from registration', () => { + const existingRegistration: LeaderboardRegistration = { + displayName: 'Test User', + gitHubUsername: 'testuser', + twitterHandle: 'testuser', + discordUsername: 'testuser#1234', + blueskyHandle: 'testuser.example.com', + submittedAt: new Date().toISOString(), + }; + + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social') as HTMLInputElement; + expect(blueskyInput.value).toBe('testuser.example.com'); + }); + + it('should handle missing Bluesky handle in existing registration', () => { + const existingRegistration: LeaderboardRegistration = { + displayName: 'Test User', + gitHubUsername: 'testuser', + twitterHandle: 'testuser', + discordUsername: 'testuser#1234', + submittedAt: new Date().toISOString(), + }; + + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social') as HTMLInputElement; + expect(blueskyInput.value).toBe(''); + }); + }); + + describe('Form submission', () => { + it('should include Bluesky handle in API submission', async () => { + // Use existing registration with Bluesky handle to test submission includes it + const existingRegistration: LeaderboardRegistration = { + displayName: 'Test User', + email: 'test@example.com', + blueskyHandle: 'testuser.bsky.social', + registeredAt: Date.now(), + emailConfirmed: true, + authToken: 'test-auth-token', + }; + + render( + + ); + + // Submit form (existing registration pre-populates fields) + const submitButton = screen.getByText('Push Up'); + await act(async () => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(window.maestro.leaderboard.submit).toHaveBeenCalledWith( + expect.objectContaining({ + blueskyHandle: 'testuser.bsky.social', + }) + ); + }); + }); + + it('should include custom domain Bluesky handle in API submission', async () => { + // Use existing registration with custom domain Bluesky handle + const existingRegistration: LeaderboardRegistration = { + displayName: 'Test User', + email: 'test@example.com', + blueskyHandle: 'user.example.com', + registeredAt: Date.now(), + emailConfirmed: true, + authToken: 'test-auth-token', + }; + + render( + + ); + + // Submit form (existing registration pre-populates fields) + const submitButton = screen.getByText('Push Up'); + await act(async () => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(window.maestro.leaderboard.submit).toHaveBeenCalledWith( + expect.objectContaining({ + blueskyHandle: 'user.example.com', + }) + ); + }); + }); + + it('should handle empty Bluesky handle (optional field)', async () => { + render( + + ); + + // Fill required fields + const displayNameInput = screen.getByPlaceholderText('ConductorPedram'); + await act(async () => { + fireEvent.change(displayNameInput, { target: { value: 'Test User' } }); + }); + + const emailInput = screen.getByPlaceholderText((content, element) => { + return element?.getAttribute('type') === 'email' || false; + }); + await act(async () => { + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + }); + + // Leave Bluesky field empty + const blueskyInput = screen.getByPlaceholderText('username.bsky.social'); + expect(blueskyInput).toHaveValue(''); + + // Submit form + const submitButton = screen.getByText('Push Up'); + await act(async () => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(window.maestro.leaderboard.submit).toHaveBeenCalledWith( + expect.objectContaining({ + blueskyHandle: undefined, + }) + ); + }); + }); + + it('should include Bluesky handle in local save', async () => { + // Use existing registration with Bluesky handle + const existingRegistration: LeaderboardRegistration = { + displayName: 'Test User', + email: 'test@example.com', + blueskyHandle: 'testuser.bsky.social', + registeredAt: Date.now(), + emailConfirmed: true, + authToken: 'test-auth-token', + }; + + render( + + ); + + // Submit form (existing registration pre-populates fields) + const submitButton = screen.getByText('Push Up'); + await act(async () => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ + blueskyHandle: 'testuser.bsky.social', + }) + ); + }); + }); + }); + + describe('Field disabled state', () => { + it('should have Bluesky field enabled when not submitting', () => { + render( + + ); + + // Verify Bluesky field is initially enabled + const blueskyInput = screen.getByPlaceholderText('username.bsky.social'); + expect(blueskyInput).not.toBeDisabled(); + }); + }); + + describe('Theme styling', () => { + it('should apply theme colors to Bluesky input', () => { + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social'); + expect(blueskyInput).toHaveStyle({ + backgroundColor: theme.colors.bgActivity, + borderColor: theme.colors.border, + color: theme.colors.textMain, + }); + }); + + it('should apply theme colors to Bluesky icon', () => { + render( + + ); + + const blueskyInput = screen.getByPlaceholderText('username.bsky.social'); + const iconContainer = blueskyInput.parentElement?.querySelector('svg'); + expect(iconContainer).toHaveStyle({ color: theme.colors.textDim }); + }); + }); + + describe('Layer stack integration', () => { + it('should register layer on mount', () => { + render( + + ); + + expect(mockRegisterLayer).toHaveBeenCalledTimes(1); + expect(mockRegisterLayer).toHaveBeenCalledWith(expect.objectContaining({ + type: 'modal', + })); + }); + + it('should unregister layer on unmount', () => { + const { unmount } = render( + + ); + + unmount(); + + expect(mockUnregisterLayer).toHaveBeenCalledWith('layer-leaderboard-123'); + }); + }); +}); diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts index f7ad4abf..942290be 100644 --- a/src/__tests__/setup.ts +++ b/src/__tests__/setup.ts @@ -394,6 +394,13 @@ const mockMaestro = { configPath: '~/.ssh/config', }), }, + leaderboard: { + submit: vi.fn().mockResolvedValue({ success: true, rank: 1 }), + pollAuthStatus: vi.fn().mockResolvedValue({ status: 'confirmed', authToken: 'test-token' }), + resendConfirmation: vi.fn().mockResolvedValue({ success: true }), + sync: vi.fn().mockResolvedValue({ success: true }), + getInstallationId: vi.fn().mockResolvedValue('test-installation-id'), + }, }; Object.defineProperty(window, 'maestro', { diff --git a/src/main/index.ts b/src/main/index.ts index fcf4f182..3cbf9692 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2104,6 +2104,8 @@ function setupIpcHandlers() { githubUsername?: string; twitterHandle?: string; linkedinHandle?: string; + discordUsername?: string; + blueskyHandle?: string; badgeLevel: number; badgeName: string; cumulativeTimeMs: number; diff --git a/src/main/preload.ts b/src/main/preload.ts index 5638feb4..b1b72f6b 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -1692,6 +1692,7 @@ contextBridge.exposeInMainWorld('maestro', { twitterHandle?: string; linkedinHandle?: string; discordUsername?: string; + blueskyHandle?: string; badgeLevel: number; badgeName: string; // Stats fields are optional for profile-only submissions (multi-device safe) @@ -2902,6 +2903,7 @@ export interface MaestroAPI { twitterHandle?: string; linkedinHandle?: string; discordUsername?: string; + blueskyHandle?: string; badgeLevel: number; badgeName: string; // Stats fields are optional for profile-only submissions (multi-device safe) diff --git a/src/renderer/components/LeaderboardRegistrationModal.tsx b/src/renderer/components/LeaderboardRegistrationModal.tsx index 17190918..306f40ca 100644 --- a/src/renderer/components/LeaderboardRegistrationModal.tsx +++ b/src/renderer/components/LeaderboardRegistrationModal.tsx @@ -44,6 +44,12 @@ const DiscordIcon = ({ className, style }: { className?: string; style?: React.C ); +const BlueskySkyIcon = ({ className, style }: { className?: string; style?: React.CSSProperties }) => ( + + + +); + interface LeaderboardRegistrationModalProps { theme: Theme; autoRunStats: AutoRunStats; @@ -94,6 +100,7 @@ export function LeaderboardRegistrationModal({ const [githubUsername, setGithubUsername] = useState(existingRegistration?.githubUsername || ''); const [linkedinHandle, setLinkedinHandle] = useState(existingRegistration?.linkedinHandle || ''); const [discordUsername, setDiscordUsername] = useState(existingRegistration?.discordUsername || ''); + const [blueskyHandle, setBlueskyHandle] = useState(existingRegistration?.blueskyHandle || ''); // Submission state const [submitState, setSubmitState] = useState('idle'); @@ -238,6 +245,7 @@ export function LeaderboardRegistrationModal({ twitterHandle: twitterHandle.trim() || undefined, linkedinHandle: linkedinHandle.trim() || undefined, discordUsername: discordUsername.trim() || undefined, + blueskyHandle: blueskyHandle.trim() || undefined, badgeLevel, badgeName, // Send cumulative stats - required by API. Server handles multi-device via delta mode. @@ -267,6 +275,7 @@ export function LeaderboardRegistrationModal({ githubUsername: githubUsername.trim() || undefined, linkedinHandle: linkedinHandle.trim() || undefined, discordUsername: discordUsername.trim() || undefined, + blueskyHandle: blueskyHandle.trim() || undefined, registeredAt: existingRegistration?.registeredAt || Date.now(), emailConfirmed: !result.pendingEmailConfirmation, lastSubmissionAt: Date.now(), @@ -815,6 +824,24 @@ export function LeaderboardRegistrationModal({ disabled={submitState === 'submitting'} /> + + {/* Bluesky */} +
+ + setBlueskyHandle(e.target.value.replace(/^@/, ''))} + placeholder="username.bsky.social" + className="flex-1 px-3 py-1.5 text-sm rounded border outline-none focus:ring-1" + style={{ + backgroundColor: theme.colors.bgActivity, + borderColor: theme.colors.border, + color: theme.colors.textMain, + }} + disabled={submitState === 'submitting'} + /> +
diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts index fb75e704..6f97b5dd 100644 --- a/src/renderer/global.d.ts +++ b/src/renderer/global.d.ts @@ -1350,6 +1350,7 @@ interface MaestroAPI { twitterHandle?: string; linkedinHandle?: string; discordUsername?: string; + blueskyHandle?: string; badgeLevel: number; badgeName: string; // Stats fields are optional for profile-only submissions (multi-device safe) diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index 7c2328d3..c0be8935 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -768,6 +768,7 @@ export interface LeaderboardRegistration { githubUsername?: string; // GitHub username linkedinHandle?: string; // LinkedIn handle discordUsername?: string; // Discord username (for @mentions in Discord posts) + blueskyHandle?: string; // Bluesky handle (username.bsky.social or custom domain) // Registration state registeredAt: number; // Timestamp when registered emailConfirmed: boolean; // Whether email has been confirmed