committed by
GitHub
32 changed files with 1064 additions and 733 deletions
@ -56,7 +56,6 @@ |
|||
"npm:react-hook-form@^7.54.2": "[email protected]", |
|||
"npm:[email protected]": "[email protected][email protected][email protected][email protected]", |
|||
"npm:react-qrcode-logo@3": "[email protected][email protected][email protected]", |
|||
"npm:react-scan@~0.2.8": "[email protected][email protected][email protected][email protected]", |
|||
"npm:react@19": "19.0.0", |
|||
"npm:rfc4648@^1.5.4": "1.5.4", |
|||
"npm:simple-git-hooks@^2.11.1": "2.11.1", |
|||
@ -899,168 +898,78 @@ |
|||
"tough-cookie" |
|||
] |
|||
}, |
|||
"@clack/[email protected]": { |
|||
"integrity": "sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==", |
|||
"dependencies": [ |
|||
"picocolors", |
|||
"sisteransi" |
|||
] |
|||
}, |
|||
"@clack/[email protected]": { |
|||
"integrity": "sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==", |
|||
"dependencies": [ |
|||
"@clack/core", |
|||
"picocolors", |
|||
"sisteransi" |
|||
] |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==" |
|||
}, |
|||
"@esbuild/[email protected]": { |
|||
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==" |
|||
}, |
|||
@ -1099,7 +1008,7 @@ |
|||
"dependencies": [ |
|||
"@inquirer/core", |
|||
"@inquirer/type", |
|||
"@types/node@22.13.8" |
|||
"@types/node" |
|||
] |
|||
}, |
|||
"@inquirer/[email protected]_@[email protected]": { |
|||
@ -1107,7 +1016,7 @@ |
|||
"dependencies": [ |
|||
"@inquirer/figures", |
|||
"@inquirer/type", |
|||
"@types/node@22.13.8", |
|||
"@types/node", |
|||
"ansi-escapes", |
|||
"cli-width", |
|||
"mute-stream", |
|||
@ -1122,7 +1031,7 @@ |
|||
"@inquirer/[email protected]_@[email protected]": { |
|||
"integrity": "sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==", |
|||
"dependencies": [ |
|||
"@types/node@22.13.8" |
|||
"@types/node" |
|||
] |
|||
}, |
|||
"@isaacs/[email protected]": { |
|||
@ -1305,29 +1214,12 @@ |
|||
"@open-draft/[email protected]": { |
|||
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==" |
|||
}, |
|||
"@pivanov/[email protected][email protected][email protected][email protected]": { |
|||
"integrity": "sha512-JQ/pXeG9/Yq3UuwH2Xp4F6bSAIDGzbxT0Vrg/82tMi3Yp+Ps9AYzjSDE+zfvBRqc7J11V6MMonUrWj4+2dYgrg==", |
|||
"dependencies": [ |
|||
"react", |
|||
"react-dom" |
|||
] |
|||
}, |
|||
"@pkgjs/[email protected]": { |
|||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==" |
|||
}, |
|||
"@polka/[email protected]": { |
|||
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==" |
|||
}, |
|||
"@preact/[email protected]": { |
|||
"integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==" |
|||
}, |
|||
"@preact/[email protected][email protected]": { |
|||
"integrity": "sha512-naxcJgUJ6BTOROJ7C3QML7KvwKwCXQJYTc5L/b0eEsdYgPB6SxwoQ1vDGcS0Q7GVjAenVq/tXrybVdFShHYZWg==", |
|||
"dependencies": [ |
|||
"@preact/signals-core", |
|||
"preact" |
|||
] |
|||
}, |
|||
"@radix-ui/[email protected]": { |
|||
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==" |
|||
}, |
|||
@ -3531,16 +3423,10 @@ |
|||
"@types/pbf" |
|||
] |
|||
}, |
|||
"@types/[email protected]": { |
|||
"integrity": "sha512-9RV2zST+0s3EhfrMZIhrz2bhuhBwxgkbHEwP2gtGWPjBzVQjifMzJ9exw7aDZhR1wbpj8zBrfp3bo8oJcGiUUw==", |
|||
"dependencies": [ |
|||
"[email protected]" |
|||
] |
|||
}, |
|||
"@types/[email protected]": { |
|||
"integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", |
|||
"dependencies": [ |
|||
"undici-types@6.20.0" |
|||
"undici-types" |
|||
] |
|||
}, |
|||
"@types/[email protected]": { |
|||
@ -3848,9 +3734,6 @@ |
|||
"[email protected]": { |
|||
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-LTCos3SmOJHrag0qF91tLUZMMw6wA+i15ESRBp71pvfNlTMYcxYoJHJ/pvFhd+29Wm5vfgVxBHV7kP5OKUUipg==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" |
|||
}, |
|||
@ -4458,64 +4341,34 @@ |
|||
"is-symbol" |
|||
] |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", |
|||
"dependencies": [ |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]", |
|||
"@esbuild/[email protected]" |
|||
] |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", |
|||
"dependencies": [ |
|||
"@esbuild/aix-ppc64@0.25.0", |
|||
"@esbuild/android-arm@0.25.0", |
|||
"@esbuild/android-arm64@0.25.0", |
|||
"@esbuild/android-x64@0.25.0", |
|||
"@esbuild/darwin-arm64@0.25.0", |
|||
"@esbuild/darwin-x64@0.25.0", |
|||
"@esbuild/freebsd-arm64@0.25.0", |
|||
"@esbuild/freebsd-x64@0.25.0", |
|||
"@esbuild/linux-arm@0.25.0", |
|||
"@esbuild/linux-arm64@0.25.0", |
|||
"@esbuild/linux-ia32@0.25.0", |
|||
"@esbuild/linux-loong64@0.25.0", |
|||
"@esbuild/linux-mips64el@0.25.0", |
|||
"@esbuild/linux-ppc64@0.25.0", |
|||
"@esbuild/linux-riscv64@0.25.0", |
|||
"@esbuild/linux-s390x@0.25.0", |
|||
"@esbuild/linux-x64@0.25.0", |
|||
"@esbuild/netbsd-arm64@0.25.0", |
|||
"@esbuild/netbsd-x64@0.25.0", |
|||
"@esbuild/openbsd-arm64@0.25.0", |
|||
"@esbuild/openbsd-x64@0.25.0", |
|||
"@esbuild/sunos-x64@0.25.0", |
|||
"@esbuild/win32-arm64@0.25.0", |
|||
"@esbuild/win32-ia32@0.25.0", |
|||
"@esbuild/win32-x64@0.25.0" |
|||
"@esbuild/aix-ppc64", |
|||
"@esbuild/android-arm", |
|||
"@esbuild/android-arm64", |
|||
"@esbuild/android-x64", |
|||
"@esbuild/darwin-arm64", |
|||
"@esbuild/darwin-x64", |
|||
"@esbuild/freebsd-arm64", |
|||
"@esbuild/freebsd-x64", |
|||
"@esbuild/linux-arm", |
|||
"@esbuild/linux-arm64", |
|||
"@esbuild/linux-ia32", |
|||
"@esbuild/linux-loong64", |
|||
"@esbuild/linux-mips64el", |
|||
"@esbuild/linux-ppc64", |
|||
"@esbuild/linux-riscv64", |
|||
"@esbuild/linux-s390x", |
|||
"@esbuild/linux-x64", |
|||
"@esbuild/netbsd-arm64", |
|||
"@esbuild/netbsd-x64", |
|||
"@esbuild/openbsd-arm64", |
|||
"@esbuild/openbsd-x64", |
|||
"@esbuild/sunos-x64", |
|||
"@esbuild/win32-arm64", |
|||
"@esbuild/win32-ia32", |
|||
"@esbuild/win32-x64" |
|||
] |
|||
}, |
|||
"[email protected]": { |
|||
@ -4701,12 +4554,6 @@ |
|||
"get-intrinsic" |
|||
] |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", |
|||
"dependencies": [ |
|||
"resolve-pkg-maps" |
|||
] |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==" |
|||
}, |
|||
@ -5139,9 +4986,6 @@ |
|||
"[email protected]": { |
|||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" |
|||
}, |
|||
@ -5340,9 +5184,6 @@ |
|||
"[email protected]": { |
|||
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==" |
|||
}, |
|||
@ -5607,9 +5448,6 @@ |
|||
"[email protected]": { |
|||
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==" |
|||
}, |
|||
@ -5777,31 +5615,6 @@ |
|||
"use-sidecar" |
|||
] |
|||
}, |
|||
"[email protected][email protected][email protected][email protected][email protected]": { |
|||
"integrity": "sha512-+6Gvu9b0UMmzV0JkigA7Y2YcjQABiNrweP9l9j8nrutN5OAYLRe4JgfwiUohPFngMD+Y6I5N0kW+okXhvVLGUw==", |
|||
"dependencies": [ |
|||
"@babel/core", |
|||
"@babel/generator", |
|||
"@babel/types", |
|||
"@clack/core", |
|||
"@clack/prompts", |
|||
"@pivanov/utils", |
|||
"@preact/signals", |
|||
"@rollup/[email protected][email protected]", |
|||
"@types/[email protected]", |
|||
"bippy", |
|||
"[email protected]", |
|||
"[email protected]", |
|||
"kleur", |
|||
"mri", |
|||
"playwright", |
|||
"preact", |
|||
"react", |
|||
"react-dom", |
|||
"tsx", |
|||
"unplugin" |
|||
] |
|||
}, |
|||
"[email protected]_@[email protected][email protected]": { |
|||
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", |
|||
"dependencies": [ |
|||
@ -5912,9 +5725,6 @@ |
|||
"[email protected]": { |
|||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", |
|||
"dependencies": [ |
|||
@ -6153,9 +5963,6 @@ |
|||
"totalist" |
|||
] |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg==" |
|||
}, |
|||
@ -6516,14 +6323,6 @@ |
|||
"[email protected]": { |
|||
"integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", |
|||
"dependencies": [ |
|||
"[email protected]", |
|||
"[email protected]", |
|||
"get-tsconfig" |
|||
] |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" |
|||
}, |
|||
@ -6601,9 +6400,6 @@ |
|||
"which-boxed-primitive" |
|||
] |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" |
|||
}, |
|||
@ -6644,13 +6440,6 @@ |
|||
"[email protected]": { |
|||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==", |
|||
"dependencies": [ |
|||
"acorn", |
|||
"webpack-virtual-modules" |
|||
] |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" |
|||
}, |
|||
@ -6747,8 +6536,8 @@ |
|||
"[email protected]_@[email protected]": { |
|||
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", |
|||
"dependencies": [ |
|||
"@types/node@22.13.8", |
|||
"esbuild@0.25.0", |
|||
"@types/node", |
|||
"esbuild", |
|||
"[email protected]", |
|||
"postcss", |
|||
"[email protected]" |
|||
@ -6757,7 +6546,7 @@ |
|||
"[email protected]_@[email protected][email protected][email protected]__@[email protected]_@[email protected][email protected][email protected]___@[email protected][email protected]___@[email protected][email protected][email protected][email protected][email protected]_____@[email protected][email protected]_____@[email protected][email protected]____@[email protected][email protected][email protected][email protected]____@[email protected][email protected][email protected][email protected][email protected][email protected]___@[email protected][email protected]___@[email protected]__@[email protected][email protected][email protected]": { |
|||
"integrity": "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==", |
|||
"dependencies": [ |
|||
"@types/node@22.13.8", |
|||
"@types/node", |
|||
"@vitest/browser", |
|||
"@vitest/expect", |
|||
"@vitest/[email protected][email protected]__@[email protected]_@[email protected][email protected][email protected]__@[email protected][email protected]", |
|||
@ -6799,9 +6588,6 @@ |
|||
"[email protected]": { |
|||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==" |
|||
}, |
|||
"[email protected]": { |
|||
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" |
|||
}, |
|||
@ -7151,7 +6937,6 @@ |
|||
"npm:react-hook-form@^7.54.2", |
|||
"npm:[email protected]", |
|||
"npm:react-qrcode-logo@3", |
|||
"npm:react-scan@~0.2.8", |
|||
"npm:react@19", |
|||
"npm:rfc4648@^1.5.4", |
|||
"npm:simple-git-hooks@^2.11.1", |
|||
|
|||
@ -1,190 +0,0 @@ |
|||
import { useAppStore } from "../../core/stores/appStore.ts"; |
|||
import { useDevice } from "../../core/stores/deviceStore.ts"; |
|||
import { |
|||
Accordion, |
|||
AccordionContent, |
|||
AccordionItem, |
|||
AccordionTrigger, |
|||
} from "../UI/Accordion.tsx"; |
|||
import { |
|||
Dialog, |
|||
DialogClose, |
|||
DialogContent, |
|||
DialogFooter, |
|||
DialogHeader, |
|||
DialogTitle, |
|||
} from "../UI/Dialog.tsx"; |
|||
import { Protobuf } from "@meshtastic/core"; |
|||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; |
|||
import { DeviceImage } from "../generic/DeviceImage.tsx"; |
|||
import { TimeAgo } from "../generic/TimeAgo.tsx"; |
|||
import { Uptime } from "../generic/Uptime.tsx"; |
|||
|
|||
export interface NodeDetailsDialogProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
} |
|||
|
|||
export const NodeDetailsDialog = ({ |
|||
open, |
|||
onOpenChange, |
|||
}: NodeDetailsDialogProps) => { |
|||
const { nodes } = useDevice(); |
|||
const { nodeNumDetails } = useAppStore(); |
|||
const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails); |
|||
|
|||
return device |
|||
? ( |
|||
<Dialog open={open} onOpenChange={onOpenChange}> |
|||
<DialogContent> |
|||
<DialogClose /> |
|||
<DialogHeader> |
|||
<DialogTitle> |
|||
Node Details for {device.user?.longName ?? "UNKNOWN"} ( |
|||
{device.user?.shortName ?? "UNK"}) |
|||
</DialogTitle> |
|||
</DialogHeader> |
|||
<DialogFooter> |
|||
<div className="w-full"> |
|||
<DeviceImage |
|||
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800" |
|||
deviceType={Protobuf.Mesh |
|||
.HardwareModel[device.user?.hwModel ?? 0]} |
|||
/> |
|||
<div className="bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
Details: |
|||
</p> |
|||
<p> |
|||
Hardware:{" "} |
|||
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]} |
|||
</p> |
|||
<p>Node Number: {device.num}</p> |
|||
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p> |
|||
<p> |
|||
Role: {Protobuf.Config.Config_DeviceConfig_Role[ |
|||
device.user?.role ?? 0 |
|||
]} |
|||
</p> |
|||
<p> |
|||
Last Heard: {device.lastHeard === 0 |
|||
? ( |
|||
"Never" |
|||
) |
|||
: <TimeAgo timestamp={device.lastHeard * 1000} />} |
|||
</p> |
|||
</div> |
|||
|
|||
{device.position |
|||
? ( |
|||
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
Position: |
|||
</p> |
|||
{device.position.latitudeI && device.position.longitudeI |
|||
? ( |
|||
<p> |
|||
Coordinates:{" "} |
|||
<a |
|||
className="text-blue-500 dark:text-blue-400" |
|||
href={`https://www.openstreetmap.org/?mlat=${device.position.latitudeI / 1e7 |
|||
}&mlon=${device.position.longitudeI / 1e7 |
|||
}&layers=N`}
|
|||
target="_blank" |
|||
rel="noreferrer" |
|||
> |
|||
{device.position.latitudeI / 1e7},{" "} |
|||
{device.position.longitudeI / 1e7} |
|||
</a> |
|||
</p> |
|||
) |
|||
: null} |
|||
{device.position.altitude |
|||
? <p>Altitude: {device.position.altitude}m</p> |
|||
: null} |
|||
</div> |
|||
) |
|||
: null} |
|||
|
|||
{device.deviceMetrics |
|||
? ( |
|||
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
Device Metrics: |
|||
</p> |
|||
{device.deviceMetrics.airUtilTx |
|||
? ( |
|||
<p> |
|||
Air TX utilization:{" "} |
|||
{device.deviceMetrics.airUtilTx.toFixed(2)}% |
|||
</p> |
|||
) |
|||
: null} |
|||
{device.deviceMetrics.channelUtilization |
|||
? ( |
|||
<p> |
|||
Channel utilization:{" "} |
|||
{device.deviceMetrics.channelUtilization.toFixed(2)}% |
|||
</p> |
|||
) |
|||
: null} |
|||
{device.deviceMetrics.batteryLevel |
|||
? ( |
|||
<p> |
|||
Battery level:{" "} |
|||
{device.deviceMetrics.batteryLevel.toFixed(2)}% |
|||
</p> |
|||
) |
|||
: null} |
|||
{device.deviceMetrics.voltage |
|||
? ( |
|||
<p> |
|||
Voltage: {device.deviceMetrics.voltage.toFixed(2)}V |
|||
</p> |
|||
) |
|||
: null} |
|||
{device.deviceMetrics.uptimeSeconds |
|||
? ( |
|||
<p> |
|||
Uptime:{" "} |
|||
<Uptime |
|||
seconds={device.deviceMetrics.uptimeSeconds} |
|||
/> |
|||
</p> |
|||
) |
|||
: null} |
|||
</div> |
|||
) |
|||
: null} |
|||
|
|||
{device |
|||
? ( |
|||
<div className="mt-5 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<Accordion |
|||
className="AccordionRoot" |
|||
type="single" |
|||
collapsible |
|||
> |
|||
<AccordionItem className="AccordionItem" value="item-1"> |
|||
<AccordionTrigger> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
All Raw Metrics: |
|||
</p> |
|||
</AccordionTrigger> |
|||
<AccordionContent className="overflow-x-scroll"> |
|||
<pre className="text-xs w-full"> |
|||
{JSON.stringify(device, null, 2)} |
|||
</pre> |
|||
</AccordionContent> |
|||
</AccordionItem> |
|||
</Accordion> |
|||
</div> |
|||
) |
|||
: null} |
|||
</div> |
|||
</DialogFooter> |
|||
</DialogContent> |
|||
</Dialog> |
|||
) |
|||
: null; |
|||
}; |
|||
@ -0,0 +1,73 @@ |
|||
import { describe, it, vi, expect, beforeEach, Mock } from "vitest"; |
|||
import { render, screen } from "@testing-library/react"; |
|||
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx"; |
|||
import { useDevice } from "@core/stores/deviceStore.ts"; |
|||
import { useAppStore } from "@core/stores/appStore.ts"; |
|||
|
|||
vi.mock("@core/stores/deviceStore"); |
|||
vi.mock("@core/stores/appStore"); |
|||
|
|||
describe("NodeDetailsDialog", () => { |
|||
const mockDevice = { |
|||
num: 1234, |
|||
user: { |
|||
longName: "Test Node", |
|||
shortName: "TN", |
|||
hwModel: 1, |
|||
role: 1, |
|||
}, |
|||
lastHeard: 1697500000, |
|||
position: { |
|||
latitudeI: 450000000, |
|||
longitudeI: -750000000, |
|||
altitude: 200, |
|||
}, |
|||
deviceMetrics: { |
|||
airUtilTx: 50.123, |
|||
channelUtilization: 75.456, |
|||
batteryLevel: 88.789, |
|||
voltage: 4.2, |
|||
uptimeSeconds: 3600, |
|||
}, |
|||
}; |
|||
|
|||
beforeEach(() => { |
|||
// Reset mocks before each test
|
|||
vi.resetAllMocks(); |
|||
|
|||
(useDevice as Mock).mockReturnValue({ |
|||
nodes: new Map([[1234, mockDevice]]), |
|||
}); |
|||
|
|||
(useAppStore as unknown as Mock).mockReturnValue({ |
|||
nodeNumDetails: 1234, |
|||
}); |
|||
}); |
|||
|
|||
it("renders node details correctly", () => { |
|||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />); |
|||
|
|||
expect(screen.getByText(/Node Details for Test Node/i)).toBeInTheDocument(); |
|||
|
|||
expect(screen.getByText("Node Number: 1234")).toBeInTheDocument(); |
|||
|
|||
expect(screen.getByText(/Air TX utilization: 50.12%/i)).toBeInTheDocument(); |
|||
expect(screen.getByText(/Channel utilization: 75.46%/i)).toBeInTheDocument(); |
|||
expect(screen.getByText(/Battery level: 88.79%/i)).toBeInTheDocument(); |
|||
expect(screen.getByText(/Voltage: 4.20V/i)).toBeInTheDocument(); |
|||
expect(screen.getByText(/Uptime:/i)).toBeInTheDocument(); |
|||
expect(screen.getByText(/Coordinates:/i)).toBeInTheDocument(); |
|||
expect(screen.getByText("45, -75")).toBeInTheDocument(); |
|||
expect(screen.getByText(/Altitude: 200m/i)).toBeInTheDocument(); |
|||
expect(screen.getByText(/Role:/i)).toBeInTheDocument(); |
|||
}); |
|||
|
|||
it("renders null if device is not found", () => { |
|||
(useDevice as Mock).mockReturnValue({ |
|||
nodes: new Map(), |
|||
}); |
|||
|
|||
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />); |
|||
expect(screen.queryByText(/Node Details for/i)).not.toBeInTheDocument(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,177 @@ |
|||
import { useAppStore } from "@core/stores/appStore.ts"; |
|||
import { useDevice } from "@core/stores/deviceStore.ts"; |
|||
import { |
|||
Accordion, |
|||
AccordionContent, |
|||
AccordionItem, |
|||
AccordionTrigger, |
|||
} from "@components/UI/Accordion.tsx"; |
|||
import { |
|||
Dialog, |
|||
DialogClose, |
|||
DialogContent, |
|||
DialogFooter, |
|||
DialogHeader, |
|||
DialogTitle, |
|||
} from "@components/UI/Dialog.tsx"; |
|||
import { Protobuf } from "@meshtastic/core"; |
|||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; |
|||
import { DeviceImage } from "@components/generic/DeviceImage.tsx"; |
|||
import { TimeAgo } from "@components/generic/TimeAgo.tsx"; |
|||
import { Uptime } from "@components/generic/Uptime.tsx"; |
|||
|
|||
export interface NodeDetailsDialogProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
} |
|||
|
|||
export const NodeDetailsDialog = ({ |
|||
open, |
|||
onOpenChange, |
|||
}: NodeDetailsDialogProps) => { |
|||
const { nodes } = useDevice(); |
|||
const { nodeNumDetails } = useAppStore(); |
|||
|
|||
const device = nodes.get(nodeNumDetails); |
|||
|
|||
if (!device) return null; |
|||
|
|||
const deviceMetricsMap = [ |
|||
{ |
|||
key: "airUtilTx", |
|||
label: "Air TX utilization", |
|||
value: device.deviceMetrics?.airUtilTx, |
|||
format: (val: number) => `${val.toFixed(2)}%`, |
|||
}, |
|||
{ |
|||
key: "channelUtilization", |
|||
label: "Channel utilization", |
|||
value: device.deviceMetrics?.channelUtilization, |
|||
format: (val: number) => `${val.toFixed(2)}%`, |
|||
}, |
|||
{ |
|||
key: "batteryLevel", |
|||
label: "Battery level", |
|||
value: device.deviceMetrics?.batteryLevel, |
|||
format: (val: number) => `${val.toFixed(2)}%`, |
|||
}, |
|||
{ |
|||
key: "voltage", |
|||
label: "Voltage", |
|||
value: device.deviceMetrics?.voltage, |
|||
format: (val: number) => `${val.toFixed(2)}V`, |
|||
}, |
|||
]; |
|||
|
|||
return ( |
|||
<Dialog open={open} onOpenChange={onOpenChange}> |
|||
<DialogContent > |
|||
<DialogClose /> |
|||
<DialogHeader> |
|||
<DialogTitle> |
|||
Node Details for {device.user?.longName ?? "UNKNOWN"} ( |
|||
{device.user?.shortName ?? "UNK"}) |
|||
</DialogTitle> |
|||
</DialogHeader> |
|||
<DialogFooter> |
|||
<div className="w-full"> |
|||
<div className="flex flex-col"> |
|||
<DeviceImage |
|||
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800" |
|||
deviceType={ |
|||
Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0] |
|||
} |
|||
/> |
|||
<div className="bg-slate-100 text-slate-900 dark:text-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold">Details:</p> |
|||
<p> |
|||
Hardware:{" "} |
|||
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]} |
|||
</p> |
|||
<p>Node Number: {device.num}</p> |
|||
<p>Node Hex: !{numberToHexUnpadded(device.num)}</p> |
|||
<p> |
|||
Role:{" "} |
|||
{ |
|||
Protobuf.Config.Config_DeviceConfig_Role[ |
|||
device.user?.role ?? 0 |
|||
] |
|||
} |
|||
</p> |
|||
<p> |
|||
Last Heard:{" "} |
|||
{device.lastHeard === 0 ? "Never" : <TimeAgo timestamp={device.lastHeard * 1000} />} |
|||
</p> |
|||
</div> |
|||
|
|||
{device.position && ( |
|||
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold">Position:</p> |
|||
{device.position.latitudeI && device.position.longitudeI && ( |
|||
<p> |
|||
Coordinates:{" "} |
|||
<a |
|||
className="text-blue-500 dark:text-blue-400" |
|||
href={`https://www.openstreetmap.org/?mlat=${device.position.latitudeI / 1e7 |
|||
}&mlon=${device.position.longitudeI / 1e7 |
|||
}&layers=N`}
|
|||
target="_blank" |
|||
rel="noreferrer" |
|||
> |
|||
{device.position.latitudeI / 1e7},{" "} |
|||
{device.position.longitudeI / 1e7} |
|||
</a> |
|||
</p> |
|||
)} |
|||
{device.position.altitude && ( |
|||
<p>Altitude: {device.position.altitude}m</p> |
|||
)} |
|||
</div> |
|||
)} |
|||
|
|||
{device.deviceMetrics && ( |
|||
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
Device Metrics: |
|||
</p> |
|||
{deviceMetricsMap.map( |
|||
(metric) => |
|||
metric.value !== undefined && ( |
|||
<p key={metric.key}> |
|||
{metric.label}: {metric.format(metric.value)} |
|||
</p> |
|||
) |
|||
)} |
|||
{device.deviceMetrics.uptimeSeconds && ( |
|||
<p> |
|||
Uptime:{" "} |
|||
<Uptime seconds={device.deviceMetrics.uptimeSeconds} /> |
|||
</p> |
|||
)} |
|||
</div> |
|||
)} |
|||
|
|||
</div> |
|||
|
|||
<div className="text-slate-900 dark:text-slate-100 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3"> |
|||
<Accordion className="AccordionRoot" type="single" collapsible> |
|||
<AccordionItem className="AccordionItem" value="item-1"> |
|||
<AccordionTrigger> |
|||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50"> |
|||
All Raw Metrics: |
|||
</p> |
|||
</AccordionTrigger> |
|||
<AccordionContent className="overflow-x-scroll"> |
|||
<pre className="text-xs w-full"> |
|||
{JSON.stringify(device, null, 2)} |
|||
</pre> |
|||
</AccordionContent> |
|||
</AccordionItem> |
|||
</Accordion> |
|||
</div> |
|||
</div> |
|||
</DialogFooter> |
|||
</DialogContent> |
|||
</Dialog> |
|||
); |
|||
}; |
|||
@ -0,0 +1,55 @@ |
|||
import { render, screen, fireEvent } from "@testing-library/react"; |
|||
import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; |
|||
import { RefreshKeysDialog } from "./RefreshKeysDialog"; |
|||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts"; |
|||
|
|||
vi.mock("./useRefreshKeysDialog.ts", () => ({ |
|||
useRefreshKeysDialog: vi.fn(), |
|||
})); |
|||
|
|||
describe("RefreshKeysDialog Component", () => { |
|||
let handleCloseDialogMock: Mock; |
|||
let handleNodeRemoveMock: Mock; |
|||
let onOpenChangeMock: Mock; |
|||
|
|||
beforeEach(() => { |
|||
handleCloseDialogMock = vi.fn(); |
|||
handleNodeRemoveMock = vi.fn(); |
|||
onOpenChangeMock = vi.fn(); |
|||
|
|||
(useRefreshKeysDialog as Mock).mockReturnValue({ |
|||
handleCloseDialog: handleCloseDialogMock, |
|||
handleNodeRemove: handleNodeRemoveMock, |
|||
}); |
|||
}); |
|||
|
|||
it("renders the dialog with correct content", () => { |
|||
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />); |
|||
expect(screen.getByText("Keys Mismatch")).toBeInTheDocument(); |
|||
expect(screen.getByText("Request New Keys")).toBeInTheDocument(); |
|||
expect(screen.getByText("Dismiss")).toBeInTheDocument(); |
|||
}); |
|||
|
|||
it("calls handleNodeRemove when 'Request New Keys' button is clicked", () => { |
|||
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />); |
|||
fireEvent.click(screen.getByText("Request New Keys")); |
|||
expect(handleNodeRemoveMock).toHaveBeenCalled(); |
|||
}); |
|||
|
|||
it("calls handleCloseDialog when 'Dismiss' button is clicked", () => { |
|||
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />); |
|||
fireEvent.click(screen.getByText("Dismiss")); |
|||
expect(handleCloseDialogMock).toHaveBeenCalled(); |
|||
}); |
|||
|
|||
it("calls onOpenChange when dialog close button is clicked", () => { |
|||
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />); |
|||
fireEvent.click(screen.getByRole("button", { name: /close/i })); |
|||
expect(handleCloseDialogMock).toHaveBeenCalled(); |
|||
}); |
|||
|
|||
it("does not render when open is false", () => { |
|||
render(<RefreshKeysDialog open={false} onOpenChange={onOpenChangeMock} />); |
|||
expect(screen.queryByText("Keys Mismatch")).not.toBeInTheDocument(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,61 @@ |
|||
import { |
|||
Dialog, |
|||
DialogClose, |
|||
DialogContent, |
|||
DialogHeader, |
|||
DialogTitle, |
|||
} from "@components/UI/Dialog.tsx"; |
|||
import { Button } from "@components/UI/Button.tsx"; |
|||
import { LockKeyholeOpenIcon } from "lucide-react"; |
|||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts"; |
|||
|
|||
export interface RefreshKeysDialogProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
} |
|||
|
|||
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => { |
|||
|
|||
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog(); |
|||
return ( |
|||
<Dialog open={open} onOpenChange={onOpenChange}> |
|||
<DialogContent className="max-w-8 flex flex-col gap-2"> |
|||
<DialogClose onClick={handleCloseDialog} /> |
|||
<DialogHeader> |
|||
<DialogTitle>Keys Mismatch</DialogTitle> |
|||
</DialogHeader> |
|||
Your node is unable to send a direct message to this node. This is due to the remote node's current public key not matching the previously stored key for this node. |
|||
<ul className="mt-2"> |
|||
<li className="flex place-items-center gap-2 items-start"> |
|||
<div className="p-2 bg-slate-500 rounded-lg mt-1"> |
|||
<LockKeyholeOpenIcon size={30} className="text-white justify-center" /> |
|||
</div> |
|||
<div className="flex flex-col gap-2"> |
|||
<div> |
|||
<p className="font-bold mb-0.5">Accept New Keys</p> |
|||
<p> |
|||
This will remove the node from device and request new keys. |
|||
</p> |
|||
</div> |
|||
<Button |
|||
variant="default" |
|||
onClick={handleNodeRemove} |
|||
className="" |
|||
> |
|||
Request New Keys |
|||
</Button> |
|||
<Button |
|||
variant="outline" |
|||
onClick={handleCloseDialog} |
|||
className="" |
|||
> |
|||
Dismiss |
|||
</Button> |
|||
</div> |
|||
</li> |
|||
</ul> |
|||
{/* </DialogDescription> */} |
|||
</DialogContent> |
|||
</Dialog > |
|||
); |
|||
}; |
|||
@ -0,0 +1,77 @@ |
|||
import { renderHook, act } from "@testing-library/react"; |
|||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts"; |
|||
import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; |
|||
import { useDevice } from "@core/stores/deviceStore.ts"; |
|||
|
|||
vi.mock("@core/stores/appStore.ts", () => ({ |
|||
useAppStore: vi.fn(() => ({ activeChat: "chat-123" })), |
|||
})); |
|||
|
|||
vi.mock("@core/stores/deviceStore.ts", () => ({ |
|||
useDevice: vi.fn(() => ({ |
|||
removeNode: vi.fn(), |
|||
setDialogOpen: vi.fn(), |
|||
getNodeError: vi.fn(), |
|||
clearNodeError: vi.fn(), |
|||
})), |
|||
})); |
|||
|
|||
describe("useRefreshKeysDialog Hook", () => { |
|||
let removeNodeMock: Mock; |
|||
let setDialogOpenMock: Mock; |
|||
let getNodeErrorMock: Mock; |
|||
let clearNodeErrorMock: Mock; |
|||
|
|||
beforeEach(() => { |
|||
removeNodeMock = vi.fn(); |
|||
setDialogOpenMock = vi.fn(); |
|||
getNodeErrorMock = vi.fn(); |
|||
clearNodeErrorMock = vi.fn(); |
|||
|
|||
(useDevice as Mock).mockReturnValue({ |
|||
removeNode: removeNodeMock, |
|||
setDialogOpen: setDialogOpenMock, |
|||
getNodeError: getNodeErrorMock, |
|||
clearNodeError: clearNodeErrorMock, |
|||
}); |
|||
}); |
|||
|
|||
it("handleNodeRemove should remove the node and update dialog if there is an error", () => { |
|||
getNodeErrorMock.mockReturnValue({ node: "node-abc" }); |
|||
|
|||
const { result } = renderHook(() => useRefreshKeysDialog()); |
|||
|
|||
act(() => { |
|||
result.current.handleNodeRemove(); |
|||
}); |
|||
|
|||
expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123"); |
|||
expect(clearNodeErrorMock).toHaveBeenCalledWith("chat-123"); |
|||
expect(removeNodeMock).toHaveBeenCalledWith("node-abc"); |
|||
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false); |
|||
}); |
|||
|
|||
it("handleNodeRemove should do nothing if there is no error", () => { |
|||
getNodeErrorMock.mockReturnValue(undefined); |
|||
|
|||
const { result } = renderHook(() => useRefreshKeysDialog()); |
|||
|
|||
act(() => { |
|||
result.current.handleNodeRemove(); |
|||
}); |
|||
|
|||
expect(removeNodeMock).not.toHaveBeenCalled(); |
|||
expect(setDialogOpenMock).not.toHaveBeenCalled(); |
|||
expect(clearNodeErrorMock).not.toHaveBeenCalled(); |
|||
}); |
|||
|
|||
it("handleCloseDialog should close the dialog", () => { |
|||
const { result } = renderHook(() => useRefreshKeysDialog()); |
|||
|
|||
act(() => { |
|||
result.current.handleCloseDialog(); |
|||
}); |
|||
|
|||
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,28 @@ |
|||
import { useCallback } from "react"; |
|||
import { useAppStore } from "@core/stores/appStore.ts"; |
|||
import { useDevice } from "@core/stores/deviceStore.ts"; |
|||
|
|||
export function useRefreshKeysDialog() { |
|||
const { removeNode, setDialogOpen, clearNodeError, getNodeError } = useDevice(); |
|||
const { activeChat } = useAppStore(); |
|||
|
|||
const handleNodeRemove = useCallback(() => { |
|||
const nodeWithError = getNodeError(activeChat); |
|||
if (!nodeWithError) { |
|||
return; |
|||
} |
|||
clearNodeError(activeChat); |
|||
handleCloseDialog();; |
|||
return removeNode(nodeWithError?.node); |
|||
}, [activeChat, clearNodeError, setDialogOpen, removeNode]); |
|||
|
|||
const handleCloseDialog = useCallback(() => { |
|||
setDialogOpen('refreshKeys', false); |
|||
}, [setDialogOpen]) |
|||
|
|||
return { |
|||
handleCloseDialog, |
|||
handleNodeRemove |
|||
}; |
|||
|
|||
} |
|||
@ -0,0 +1,126 @@ |
|||
import { |
|||
Tooltip, |
|||
TooltipArrow, |
|||
TooltipContent, |
|||
TooltipProvider, |
|||
TooltipTrigger, |
|||
} from "@components/UI/Tooltip.tsx"; |
|||
import { useDeviceStore } from "@core/stores/deviceStore.ts"; |
|||
import { cn } from "@core/utils/cn.ts"; |
|||
import { Avatar } from "@components/UI/Avatar.tsx"; |
|||
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react"; |
|||
import type { LucideIcon } from "lucide-react"; |
|||
import { ReactNode, useMemo } from "react"; |
|||
import { Message, MessageState } from "@core/services/types.ts"; |
|||
|
|||
interface MessageProps { |
|||
lastMsgSameUser: boolean; |
|||
message: Message; |
|||
} |
|||
|
|||
interface MessageStatus { |
|||
state: MessageState; |
|||
displayText: string; |
|||
icon: LucideIcon; |
|||
} |
|||
|
|||
const MESSAGE_STATUS: Record<MessageState, MessageStatus> = { |
|||
ack: { state: "ack", displayText: "Message delivered", icon: CheckCircle2 }, |
|||
waiting: { state: "waiting", displayText: "Waiting for delivery", icon: CircleEllipsis }, |
|||
failed: { state: "failed", displayText: "Delivery failed", icon: AlertCircle }, |
|||
}; |
|||
|
|||
const getMessageStatus = (state: MessageState): MessageStatus => |
|||
MESSAGE_STATUS[state] || { state: "failed", displayText: "Unknown error", icon: AlertCircle }; |
|||
|
|||
const StatusTooltip = ({ status, children }: { status: MessageStatus; children: ReactNode }) => ( |
|||
<TooltipProvider> |
|||
<Tooltip> |
|||
<TooltipTrigger asChild>{children}</TooltipTrigger> |
|||
<TooltipContent |
|||
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95" |
|||
side="top" |
|||
align="center" |
|||
sideOffset={5} |
|||
> |
|||
{status.displayText} |
|||
<TooltipArrow className="fill-slate-800" /> |
|||
</TooltipContent> |
|||
</Tooltip> |
|||
</TooltipProvider> |
|||
); |
|||
|
|||
const StatusIcon = ({ status, className, ...otherProps }: { status: MessageStatus; className?: string }) => { |
|||
const isFailed = status.state === "failed"; |
|||
const iconClass = cn("text-slate-500 dark:text-slate-400 w-4 h-4 shrink-0", className); |
|||
const Icon = status.icon; |
|||
|
|||
return ( |
|||
<StatusTooltip status={status}> |
|||
<Icon className={iconClass} {...otherProps} color={isFailed ? "red" : "currentColor"} /> |
|||
</StatusTooltip> |
|||
); |
|||
}; |
|||
|
|||
const getMessageTextStyles = (status: MessageStatus) => { |
|||
const isAcknowledged = status.state === "ack"; |
|||
const isFailed = status.state === "failed"; |
|||
|
|||
return cn( |
|||
"break-words overflow-hidden", |
|||
isAcknowledged ? "text-slate-900 dark:text-white" : "text-slate-900 dark:text-slate-400", |
|||
isFailed && "text-red-500 dark:text-red-500", |
|||
); |
|||
}; |
|||
|
|||
const TimeDisplay = ({ date, className }: { date: Date; className?: string }) => ( |
|||
<div className={cn("flex items-center gap-2 shrink-0", className)}> |
|||
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">{date.toLocaleDateString()}</span> |
|||
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono"> |
|||
{date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })} |
|||
</span> |
|||
</div> |
|||
); |
|||
|
|||
export const MessageItem = ({ lastMsgSameUser, message }: MessageProps) => { |
|||
const { getDevices } = useDeviceStore(); |
|||
|
|||
const isDeviceUser = useMemo( |
|||
() => |
|||
getDevices() |
|||
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num) |
|||
.includes(message.from), |
|||
[getDevices, message.from], |
|||
); |
|||
|
|||
const messageUser = message?.from |
|||
? getDevices().find((device) => device.nodes.has(message.from))?.nodes.get(message.from) |
|||
: null; |
|||
|
|||
const messageStatus = getMessageStatus(message.state); |
|||
const messageTextClass = getMessageTextStyles(messageStatus); |
|||
|
|||
return ( |
|||
<div className="flex flex-col w-full px-4 justify-start"> |
|||
<div className={cn("flex flex-col flex-wrap items-start py-1", messageTextClass, isDeviceUser && "items-end")}> |
|||
<div className="flex items-center gap-2 mb-2"> |
|||
{!lastMsgSameUser && ( |
|||
<div className="flex place-items-center gap-2 mb-1"> |
|||
<Avatar text={messageUser?.user?.shortName ?? "UNK"} /> |
|||
<div className="flex flex-col"> |
|||
<span className="font-medium text-slate-900 dark:text-white truncate"> |
|||
{messageUser?.user?.longName} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
<TimeDisplay date={message.date} /> |
|||
<div className="flex place-items-center gap-2 pb-2"> |
|||
<div className={cn(isDeviceUser && "pl-11", messageTextClass)}>{message.message}</div> |
|||
<StatusIcon status={messageStatus} /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,95 @@ |
|||
import { describe, it, expect, vi, beforeEach, Mock } from "vitest"; |
|||
import { render, screen } from "@testing-library/react"; |
|||
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx"; |
|||
import { useDevice } from "@core/stores/deviceStore.ts"; |
|||
|
|||
vi.mock("@core/stores/deviceStore"); |
|||
|
|||
describe("TraceRoute", () => { |
|||
const mockNodes = new Map([ |
|||
[ |
|||
1, |
|||
{ num: 1, user: { longName: "Node A" } }, |
|||
], |
|||
[ |
|||
2, |
|||
{ num: 2, user: { longName: "Node B" } }, |
|||
], |
|||
[ |
|||
3, |
|||
{ num: 3, user: { longName: "Node C" } }, |
|||
], |
|||
]); |
|||
|
|||
beforeEach(() => { |
|||
vi.resetAllMocks(); |
|||
(useDevice as Mock).mockReturnValue({ |
|||
nodes: mockNodes, |
|||
}); |
|||
}); |
|||
|
|||
it("renders the route to destination with SNR values", () => { |
|||
render( |
|||
<TraceRoute |
|||
from={{ user: { longName: "Source Node" } } as any} |
|||
to={{ user: { longName: "Destination Node" } } as any} |
|||
route={[1, 2]} |
|||
snrTowards={[10, 20, 30]} |
|||
/> |
|||
); |
|||
|
|||
expect(screen.getByText("Route to destination:")).toBeInTheDocument(); |
|||
expect(screen.getByText("Destination Node")).toBeInTheDocument(); |
|||
|
|||
expect(screen.getByText("Node A")).toBeInTheDocument(); |
|||
expect(screen.getByText("Node B")).toBeInTheDocument(); |
|||
|
|||
expect(screen.getAllByText(/↓/)).toHaveLength(3); // startNode + 2 hops
|
|||
expect(screen.getByText("↓ 10dB")).toBeInTheDocument(); |
|||
expect(screen.getByText("↓ 20dB")).toBeInTheDocument(); |
|||
expect(screen.getByText("↓ 30dB")).toBeInTheDocument(); |
|||
expect(screen.getByText("Source Node")).toBeInTheDocument(); |
|||
}); |
|||
|
|||
it("renders the route back when provided", () => { |
|||
render( |
|||
<TraceRoute |
|||
from={{ user: { longName: "Source Node" } } as any} |
|||
to={{ user: { longName: "Destination Node" } } as any} |
|||
route={[1]} |
|||
snrTowards={[15, 25]} |
|||
routeBack={[3]} |
|||
snrBack={[35, 45]} |
|||
/> |
|||
); |
|||
|
|||
expect(screen.getByText("Route back:")).toBeInTheDocument(); |
|||
expect(screen.getByText("Node C")).toBeInTheDocument(); |
|||
expect(screen.getByText("↓ 35dB")).toBeInTheDocument(); |
|||
expect(screen.getByText("↓ 45dB")).toBeInTheDocument(); |
|||
}); |
|||
|
|||
it("renders '??' for missing SNR values", () => { |
|||
render( |
|||
<TraceRoute |
|||
from={{ user: { longName: "Source" } } as any} |
|||
to={{ user: { longName: "Dest" } } as any} |
|||
route={[1]} |
|||
/> |
|||
); |
|||
|
|||
expect(screen.getAllByText("↓ ??dB").length).toBeGreaterThan(0); |
|||
}); |
|||
|
|||
it("renders hop hex if node is not found", () => { |
|||
render( |
|||
<TraceRoute |
|||
from={{ user: { longName: "Source" } } as any} |
|||
to={{ user: { longName: "Dest" } } as any} |
|||
route={[99]} |
|||
/> |
|||
); |
|||
|
|||
expect(screen.getByText(/^!63$/)).toBeInTheDocument(); // 99 in hex
|
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue