mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> Co-authored-by: Pedram Amini <pedram.amini@gmail.com>
This commit is contained in:
committed by
GitHub
parent
9572042da6
commit
d7ddbab52c
80
package-lock.json
generated
80
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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> = {}): 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> = {}): 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<typeof vi.fn>;
|
||||
let onSave: ReturnType<typeof vi.fn>;
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
const blueskyInput = screen.getByPlaceholderText('username.bsky.social');
|
||||
expect(blueskyInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render Bluesky icon with correct styling', () => {
|
||||
render(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={existingRegistration}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={existingRegistration}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={existingRegistration}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={existingRegistration}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={existingRegistration}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={existingRegistration}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
// 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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(mockRegisterLayer).toHaveBeenCalledTimes(1);
|
||||
expect(mockRegisterLayer).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'modal',
|
||||
}));
|
||||
});
|
||||
|
||||
it('should unregister layer on unmount', () => {
|
||||
const { unmount } = render(
|
||||
<LeaderboardRegistrationModal
|
||||
theme={theme}
|
||||
autoRunStats={autoRunStats}
|
||||
keyboardMasteryStats={keyboardMasteryStats}
|
||||
existingRegistration={null}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockUnregisterLayer).toHaveBeenCalledWith('layer-leaderboard-123');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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', {
|
||||
|
||||
@@ -2104,6 +2104,8 @@ function setupIpcHandlers() {
|
||||
githubUsername?: string;
|
||||
twitterHandle?: string;
|
||||
linkedinHandle?: string;
|
||||
discordUsername?: string;
|
||||
blueskyHandle?: string;
|
||||
badgeLevel: number;
|
||||
badgeName: string;
|
||||
cumulativeTimeMs: number;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -44,6 +44,12 @@ const DiscordIcon = ({ className, style }: { className?: string; style?: React.C
|
||||
</svg>
|
||||
);
|
||||
|
||||
const BlueskySkyIcon = ({ className, style }: { className?: string; style?: React.CSSProperties }) => (
|
||||
<svg className={className} style={style} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
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<SubmitState>('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'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bluesky */}
|
||||
<div className="flex items-center gap-2">
|
||||
<BlueskySkyIcon className="w-4 h-4 flex-shrink-0" style={{ color: theme.colors.textDim }} />
|
||||
<input
|
||||
type="text"
|
||||
value={blueskyHandle}
|
||||
onChange={(e) => 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'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
1
src/renderer/global.d.ts
vendored
1
src/renderer/global.d.ts
vendored
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user