diff --git a/package.json b/package.json index 0486550..4e636c4 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dev:test:mp-weixin": "uni -p mp-weixin --mode test", "build:test:mp-weixin": "uni build -p mp-weixin --mode test", "build:mp-weixin": "uni build -p mp-weixin --mode production", + "test:unit": "vitest run", "type-check": "vue-tsc --noEmit", "lint": "eslint \"src/**/*.{ts,vue}\"" }, @@ -39,9 +40,11 @@ "@vue/runtime-core": "3.4.21", "eslint": "8.57.1", "eslint-plugin-vue": "9.30.0", + "sass": "1.99.0", "sass-embedded": "1.99.0", "typescript": "5.3.3", "vite": "5.2.8", + "vitest": "1.6.1", "vue-eslint-parser": "9.4.3", "vue-tsc": "1.8.27" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 554fbfe..f9123c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: eslint-plugin-vue: specifier: 9.30.0 version: 9.30.0(eslint@8.57.1) + sass: + specifier: 1.99.0 + version: 1.99.0 sass-embedded: specifier: 1.99.0 version: 1.99.0 @@ -69,6 +72,9 @@ importers: vite: specifier: 5.2.8 version: 5.2.8(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1) + vitest: + specifier: 1.6.1 + version: 1.6.1(@types/node@25.9.1)(jsdom@16.7.0)(sass@1.99.0)(terser@5.47.1) vue-eslint-parser: specifier: 9.4.3 version: 9.4.3(eslint@8.57.1) @@ -1046,6 +1052,10 @@ packages: node-notifier: optional: true + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/source-map@27.5.1': resolution: {integrity: sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -1497,6 +1507,9 @@ packages: cpu: [x64] os: [win32] + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@sinonjs/commons@1.8.6': resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} @@ -1652,6 +1665,21 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 + '@vitest/expect@1.6.1': + resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + + '@vitest/runner@1.6.1': + resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + + '@vitest/snapshot@1.6.1': + resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + + '@vitest/spy@1.6.1': + resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + + '@vitest/utils@1.6.1': + resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + '@volar/language-core@1.11.1': resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} @@ -1756,6 +1784,10 @@ packages: resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} engines: {node: '>=0.4.0'} + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + acorn@7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} @@ -1821,6 +1853,9 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1942,6 +1977,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cac@6.7.9: resolution: {integrity: sha512-XN5qEpfNQCJ8jRaZgitSkkukjMRCGio+X3Ks5KUbGGlPbV+pSem1l9VuzooCBXOiMFshUZgyYqg6rgN8rjkb/w==} engines: {node: '>=8'} @@ -1972,6 +2011,10 @@ packages: centra@2.7.0: resolution: {integrity: sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==} + chai@4.3.10: + resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} + engines: {node: '>=4'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1984,6 +2027,9 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2161,6 +2207,10 @@ packages: dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2196,6 +2246,10 @@ packages: resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2259,8 +2313,8 @@ packages: es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: @@ -2359,6 +2413,10 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + exif-parser@0.1.12: resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} @@ -2480,6 +2538,9 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2496,6 +2557,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2583,6 +2648,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2679,6 +2748,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -2949,6 +3022,10 @@ packages: resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} engines: {node: '>= 12.13.0'} + local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + local-pkg@1.2.1: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} @@ -2977,6 +3054,9 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3051,6 +3131,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + min-document@2.19.2: resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} @@ -3117,6 +3201,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -3141,6 +3229,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3157,6 +3249,10 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -3217,6 +3313,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -3227,9 +3327,15 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + phin@2.9.3: resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -3353,6 +3459,10 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -3407,6 +3517,9 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.0.0: + resolution: {integrity: sha512-yUcBYdBBbo3QiPsgYDcfQcIkGZHfxOaoE6HLSnr1sPzMhdyxusbfKOSUbSd/ocGi32dxcj366PsTj+5oggeKKw==} + read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -3684,9 +3797,16 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -3716,10 +3836,16 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.5.0: + resolution: {integrity: sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA==} + string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} @@ -3743,10 +3869,17 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -3813,9 +3946,20 @@ packages: timm@1.7.1: resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} + tinybench@2.5.1: + resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinypool@0.8.3: + resolution: {integrity: sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==} + engines: {node: '>=14.0.0'} + + tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -3970,6 +4114,11 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@1.6.1: + resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite@5.2.8: resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3998,6 +4147,31 @@ packages: terser: optional: true + vitest@1.6.1: + resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.1 + '@vitest/ui': 1.6.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vue-eslint-parser@9.4.3: resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} engines: {node: ^14.17.0 || >=16.0.0} @@ -4063,6 +4237,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4159,6 +4338,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + snapshots: '@ampproject/remapping@2.3.0': @@ -4374,7 +4557,7 @@ snapshots: '@babel/helper-validator-identifier': 7.28.5 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.1.0 + picocolors: 1.1.1 '@babel/parser@7.25.6': dependencies: @@ -5693,6 +5876,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.10 + '@jest/source-map@27.5.1': dependencies: callsites: 3.1.0 @@ -6202,6 +6389,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true + '@sinclair/typebox@0.27.10': {} + '@sinonjs/commons@1.8.6': dependencies: type-detect: 4.0.8 @@ -6396,6 +6585,35 @@ snapshots: vite: 5.2.8(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1) vue: 3.4.21(typescript@5.3.3) + '@vitest/expect@1.6.1': + dependencies: + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + chai: 4.3.10 + + '@vitest/runner@1.6.1': + dependencies: + '@vitest/utils': 1.6.1 + p-limit: 5.0.0 + pathe: 1.1.2 + + '@vitest/snapshot@1.6.1': + dependencies: + magic-string: 0.30.21 + pathe: 1.1.2 + pretty-format: 29.7.0 + + '@vitest/spy@1.6.1': + dependencies: + tinyspy: 2.2.1 + + '@vitest/utils@1.6.1': + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + '@volar/language-core@1.11.1': dependencies: '@volar/source-map': 1.11.1 @@ -6559,6 +6777,10 @@ snapshots: acorn-walk@7.2.0: {} + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + acorn@7.4.1: {} acorn@8.16.0: {} @@ -6613,6 +6835,8 @@ snapshots: array-union@2.1.0: {} + assertion-error@1.1.0: {} + asynckit@0.4.0: {} autoprefixer@10.4.20(postcss@8.5.15): @@ -6779,6 +7003,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + cac@6.7.9: {} call-bind-apply-helpers@1.0.2: @@ -6805,6 +7031,16 @@ snapshots: transitivePeerDependencies: - debug + chai@4.3.10: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -6818,6 +7054,10 @@ snapshots: char-regex@1.0.2: {} + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -6833,7 +7073,6 @@ snapshots: chokidar@4.0.3: dependencies: readdirp: 4.1.2 - optional: true ci-info@3.9.0: {} @@ -6959,6 +7198,10 @@ snapshots: dedent@0.7.0: {} + deep-eql@4.1.4: + dependencies: + type-detect: 4.0.8 + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -6980,6 +7223,8 @@ snapshots: diff-sequences@27.5.1: {} + diff-sequences@29.6.3: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -7026,7 +7271,7 @@ snapshots: es-module-lexer@1.5.4: {} - es-object-atoms@1.1.1: + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -7035,7 +7280,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.3 + hasown: 2.0.4 esbuild@0.20.2: optionalDependencies: @@ -7187,6 +7432,18 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + exif-parser@0.1.12: {} exit@0.1.2: {} @@ -7337,17 +7594,19 @@ snapshots: get-caller-file@2.0.5: {} + get-func-name@2.0.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-package-type@0.1.0: {} @@ -7355,10 +7614,12 @@ snapshots: get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-stream@6.0.1: {} + get-stream@8.0.1: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7453,6 +7714,8 @@ snapshots: human-signals@2.1.0: {} + human-signals@5.0.0: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -7522,6 +7785,8 @@ snapshots: is-stream@2.0.1: {} + is-stream@3.0.0: {} + is-typedarray@1.0.0: {} isbinaryfile@5.0.2: {} @@ -8038,6 +8303,11 @@ snapshots: loader-utils@3.3.1: {} + local-pkg@0.5.0: + dependencies: + mlly: 1.8.2 + pkg-types: 1.3.1 + local-pkg@1.2.1: dependencies: mlly: 1.8.2 @@ -8062,6 +8332,10 @@ snapshots: lodash@4.18.1: {} + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -8117,6 +8391,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-fn@4.0.0: {} + min-document@2.19.2: dependencies: dom-walk: 0.1.2 @@ -8171,6 +8447,10 @@ snapshots: dependencies: path-key: 3.1.1 + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -8193,6 +8473,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -8214,6 +8498,10 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@5.0.0: + dependencies: + yocto-queue: 1.2.2 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -8270,14 +8558,20 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-to-regexp@0.1.10: {} path-type@4.0.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@1.1.1: {} + phin@2.9.3: {} phin@3.7.1: @@ -8399,6 +8693,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.0.0 + process@0.11.10: {} prompts@2.4.2: @@ -8446,6 +8746,8 @@ snapshots: react-is@17.0.2: {} + react-is@18.0.0: {} + read-cache@1.0.0: dependencies: pify: 2.3.0 @@ -8454,8 +8756,7 @@ snapshots: dependencies: picomatch: 2.3.2 - readdirp@4.1.2: - optional: true + readdirp@4.1.2: {} regenerate-unicode-properties@10.2.2: dependencies: @@ -8654,7 +8955,6 @@ snapshots: source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.6 - optional: true sax@1.6.0: {} @@ -8749,8 +9049,12 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -8772,8 +9076,12 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.5.0: {} + string-hash@1.1.3: {} string-length@4.0.2: @@ -8795,8 +9103,14 @@ snapshots: strip-final-newline@2.0.0: {} + strip-final-newline@3.0.0: {} + strip-json-comments@3.1.1: {} + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -8858,8 +9172,14 @@ snapshots: timm@1.7.1: {} + tinybench@2.5.1: {} + tinycolor2@1.6.0: {} + tinypool@0.8.3: {} + + tinyspy@2.2.1: {} + tmpl@1.0.5: {} to-fast-properties@2.0.0: {} @@ -9002,6 +9322,23 @@ snapshots: vary@1.1.2: {} + vite-node@1.6.1(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 5.2.8(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + vite@5.2.8(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1): dependencies: esbuild: 0.20.2 @@ -9013,6 +9350,40 @@ snapshots: sass: 1.99.0 terser: 5.47.1 + vitest@1.6.1(@types/node@25.9.1)(jsdom@16.7.0)(sass@1.99.0)(terser@5.47.1): + dependencies: + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.5 + chai: 4.3.10 + debug: 4.4.3 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.21 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.5.0 + strip-literal: 2.1.1 + tinybench: 2.5.1 + tinypool: 0.8.3 + vite: 5.2.8(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1) + vite-node: 1.6.1(@types/node@25.9.1)(sass@1.99.0)(terser@5.47.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.1 + jsdom: 16.7.0 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + vue-eslint-parser@9.4.3(eslint@8.57.1): dependencies: debug: 4.4.3 @@ -9087,6 +9458,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -9157,3 +9533,5 @@ snapshots: yargs-parser: 20.2.9 yocto-queue@0.1.0: {} + + yocto-queue@1.2.2: {} diff --git a/src/components/navigation/ArrivalPanel.vue b/src/components/navigation/ArrivalPanel.vue index 799dcac..26c6473 100644 --- a/src/components/navigation/ArrivalPanel.vue +++ b/src/components/navigation/ArrivalPanel.vue @@ -69,7 +69,7 @@ {{ primaryActionText }} @@ -124,10 +124,6 @@ import { THIRD_PARTY_MAP_PROVIDERS, type ThirdPartyMapProviderOption } from '@/services/ThirdPartyMapSearchService' -import { - isNativeWechatMiniProgram, - isWechatMiniProgramRuntime -} from '@/utils/hostEnvironment' const props = withDefaults(defineProps<{ visible?: boolean @@ -151,13 +147,13 @@ const emit = defineEmits<{ const typeOptions = computed(() => ARRIVAL_TARGET_TYPES) const mapProviders = computed(() => THIRD_PARTY_MAP_PROVIDERS) -const shouldUseMiniProgramLocation = computed(() => isWechatMiniProgramRuntime()) const primaryActionText = computed(() => ( - shouldUseMiniProgramLocation.value ? '打开地图导航' : '第三方导航' + isOpeningNavigation.value ? '正在打开...' : '打开地图导航' )) const arrivalPanelRef = ref(null) const isCollapsed = ref(false) const providerSheetVisible = ref(false) +const isOpeningNavigation = ref(false) let resizeObserver: ResizeObserver | null = null const activeTypeLabel = computed(() => ( @@ -243,6 +239,7 @@ const startLayoutObserver = () => { } const handleNavigateTap = async () => { + if (isOpeningNavigation.value) return if (!props.selectedTarget) { uni.showToast({ title: '请先选择点位', @@ -257,14 +254,15 @@ const handleNavigateTap = async () => { name: props.selectedTarget.title, address: props.selectedTarget.subtitle } - const isNativeMiniProgram = isNativeWechatMiniProgram() - const openedInMiniProgram = await openWechatMiniProgramLocation(target) - - if (openedInMiniProgram || isNativeMiniProgram || isWechatMiniProgramRuntime()) { - return + isOpeningNavigation.value = true + try { + const result = await openWechatMiniProgramLocation(target) + if (result.status === 'not-embedded') { + providerSheetVisible.value = true + } + } finally { + isOpeningNavigation.value = false } - - providerSheetVisible.value = true } const handleProviderSelect = (provider: ThirdPartyMapProviderOption) => { diff --git a/src/pages/open-location/index.vue b/src/pages/open-location/index.vue index 3833a29..1c95792 100644 --- a/src/pages/open-location/index.vue +++ b/src/pages/open-location/index.vue @@ -16,15 +16,13 @@ diff --git a/src/services/ThirdPartyMapSearchService.ts b/src/services/ThirdPartyMapSearchService.ts index f1b3c24..5e5c76c 100644 --- a/src/services/ThirdPartyMapSearchService.ts +++ b/src/services/ThirdPartyMapSearchService.ts @@ -3,6 +3,11 @@ import { isEmbeddedInWechatMiniProgram, isNativeWechatMiniProgram } from '@/utils/hostEnvironment' +import { + buildWechatOpenLocationPageUrl, + validateWechatOpenLocationTarget, + type WechatOpenLocationTarget +} from '@/utils/wechatOpenLocationProtocol' export type ThirdPartyMapProvider = 'amap' | 'baidu' | 'tencent' @@ -11,12 +16,7 @@ export interface ThirdPartyMapSearchTarget { region: string } -export interface WechatMiniProgramLocationTarget { - latitude: number - longitude: number - name: string - address?: string -} +export type WechatMiniProgramLocationTarget = WechatOpenLocationTarget interface WechatMiniProgramBridge { getEnv?: (callback: (result: { miniprogram?: boolean }) => void) => void @@ -27,7 +27,17 @@ interface WechatMiniProgramBridge { }) => void } -let wechatJsSdkLoadPromise: Promise | null = null +export type WechatMiniProgramLocationOpenResult = + | { status: 'opened' } + | { status: 'not-embedded' } + | { status: 'failed'; reason: 'invalid-target' | 'bridge-unavailable' | 'not-in-mini-program' | 'navigation-failed' } + +const BRIDGE_WAIT_TIMEOUT = 2500 +const BRIDGE_POLL_INTERVAL = 50 +const SDK_SCRIPT_LOAD_TIMEOUT = 10000 +const GET_ENV_TIMEOUT = 1500 +const NAVIGATION_TIMEOUT = 3000 +let miniProgramNavigationPromise: Promise | null = null export interface ThirdPartyMapProviderOption { provider: ThirdPartyMapProvider @@ -82,18 +92,7 @@ export const openThirdPartyMapSearch = ( export const buildWechatMiniProgramOpenLocationUrl = ( target: WechatMiniProgramLocationTarget -) => { - const params = [ - ['latitude', String(target.latitude)], - ['longitude', String(target.longitude)], - ['name', target.name], - ['address', target.address || ''] - ] - .map(([key, value]) => `${key}=${encode(value)}`) - .join('&') - - return `/pages/open-location/index?${params}` -} +) => buildWechatOpenLocationPageUrl(target) const getWechatMiniProgramBridge = (): WechatMiniProgramBridge | null => { if (typeof window === 'undefined') return null @@ -105,29 +104,35 @@ const getWechatMiniProgramBridge = (): WechatMiniProgramBridge | null => { }).wx?.miniProgram || null } -const loadWechatJsSdk = () => { - if (typeof document === 'undefined') return Promise.resolve(false) - if (getWechatMiniProgramBridge()) return Promise.resolve(true) - if (wechatJsSdkLoadPromise) return wechatJsSdkLoadPromise +const ensureWechatJsSdkScript = () => { + if (typeof document === 'undefined' || getWechatMiniProgramBridge()) return + const existingScript = document.querySelector('script[data-wechat-js-sdk="true"]') + if (existingScript) { + const startedAt = Number(existingScript.dataset.wechatJsSdkStartedAt || 0) + const isExpiredLoading = existingScript.dataset.wechatJsSdkState === 'loading' + && Date.now() - startedAt > SDK_SCRIPT_LOAD_TIMEOUT + if (existingScript.dataset.wechatJsSdkState !== 'error' && !isExpiredLoading) return + existingScript.remove() + } - wechatJsSdkLoadPromise = new Promise((resolve) => { - const existingScript = document.querySelector('script[data-wechat-js-sdk="true"]') - if (existingScript) { - existingScript.addEventListener('load', () => resolve(Boolean(getWechatMiniProgramBridge())), { once: true }) - existingScript.addEventListener('error', () => resolve(false), { once: true }) - return - } - - const script = document.createElement('script') - script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js' - script.async = true - script.dataset.wechatJsSdk = 'true' - script.onload = () => resolve(Boolean(getWechatMiniProgramBridge())) - script.onerror = () => resolve(false) + const script = document.createElement('script') + script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js' + script.async = true + script.dataset.wechatJsSdk = 'true' + script.dataset.wechatJsSdkState = 'loading' + script.dataset.wechatJsSdkStartedAt = String(Date.now()) + script.onload = () => { + script.dataset.wechatJsSdkState = 'loaded' + } + script.onerror = () => { + script.dataset.wechatJsSdkState = 'error' + script.remove() + } + try { document.head.appendChild(script) - }) - - return wechatJsSdkLoadPromise + } catch (error) { + console.error('加载微信 JS SDK 失败:', error) + } } const isWechatBrowser = () => ( @@ -135,6 +140,25 @@ const isWechatBrowser = () => ( && /MicroMessenger/i.test(window.navigator?.userAgent || '') ) +const wait = (duration: number) => new Promise((resolve) => { + setTimeout(resolve, duration) +}) + +const waitForWechatMiniProgramBridge = async () => { + const immediateBridge = getWechatMiniProgramBridge() + if (immediateBridge) return immediateBridge + + ensureWechatJsSdkScript() + const startedAt = Date.now() + while (Date.now() - startedAt < BRIDGE_WAIT_TIMEOUT) { + await wait(BRIDGE_POLL_INTERVAL) + const bridge = getWechatMiniProgramBridge() + if (bridge) return bridge + } + + return null +} + const resolveMiniProgramWebViewEnvironment = ( miniProgram: WechatMiniProgramBridge ) => { @@ -150,13 +174,13 @@ const resolveMiniProgramWebViewEnvironment = ( clearTimeout(timeout) resolve(isMiniProgram) } - const timeout = setTimeout(() => finish(false), 1500) + const timeout = setTimeout(() => finish(isEmbeddedInWechatMiniProgram()), GET_ENV_TIMEOUT) try { - getEnv((result) => finish(Boolean(result.miniprogram))) + getEnv((result) => finish(Boolean(result?.miniprogram) || isEmbeddedInWechatMiniProgram())) } catch (error) { console.error('识别小程序 web-view 环境失败:', error) - finish(false) + // 保留超时重检,让延迟注入的宿主环境标识仍有机会生效。 } }) } @@ -164,50 +188,127 @@ const resolveMiniProgramWebViewEnvironment = ( const navigateToMiniProgramLocationPage = ( miniProgram: WechatMiniProgramBridge, target: WechatMiniProgramLocationTarget -) => new Promise((resolve) => { +) => new Promise((resolve) => { + let settled = false + const finish = (result: WechatMiniProgramLocationOpenResult) => { + if (settled) return + settled = true + clearTimeout(timeout) + resolve(result) + } + const timeout = setTimeout(() => { + uni.showToast({ + title: '宿主地图页响应超时', + icon: 'none' + }) + finish({ status: 'failed', reason: 'navigation-failed' }) + }, NAVIGATION_TIMEOUT) + if (!miniProgram.navigateTo) { - resolve(false) + uni.showToast({ + title: '宿主地图桥接不可用', + icon: 'none' + }) + finish({ status: 'failed', reason: 'bridge-unavailable' }) return } - miniProgram.navigateTo({ - url: buildWechatMiniProgramOpenLocationUrl(target), - success: () => resolve(true), - fail: (error) => { - console.error('跳转小程序地图页失败:', error) - uni.showToast({ - title: '无法打开地图导航', - icon: 'none' - }) - resolve(false) - } - }) + try { + miniProgram.navigateTo({ + url: buildWechatMiniProgramOpenLocationUrl(target), + success: () => finish({ status: 'opened' }), + fail: (error) => { + if (settled) return + console.error('跳转小程序地图页失败:', error) + const errorMessage = typeof error === 'object' && error && 'errMsg' in error + ? String(error.errMsg) + : '' + uni.showToast({ + title: /page.*not found|not found.*page/i.test(errorMessage) + ? '宿主小程序未配置地图页' + : '宿主地图页跳转失败', + icon: 'none' + }) + finish({ status: 'failed', reason: 'navigation-failed' }) + } + }) + } catch (error) { + console.error('调用小程序地图桥接失败:', error) + uni.showToast({ + title: '宿主地图桥接调用失败', + icon: 'none' + }) + finish({ status: 'failed', reason: 'navigation-failed' }) + } }) -export const openWechatMiniProgramLocation = async ( +const openWechatMiniProgramLocationOnce = async ( target: WechatMiniProgramLocationTarget -) => { - if (isNativeWechatMiniProgram()) { - return openExternalNavigation(target) +): Promise => { + const validation = validateWechatOpenLocationTarget(target) + if (!validation.ok) { + uni.showToast({ + title: validation.message, + icon: 'none' + }) + return { status: 'failed', reason: 'invalid-target' } } - const synchronouslyEmbedded = isEmbeddedInWechatMiniProgram() - if (!synchronouslyEmbedded && !isWechatBrowser()) return false - - await loadWechatJsSdk() - const miniProgram = getWechatMiniProgramBridge() - if (!miniProgram?.navigateTo) { - if (synchronouslyEmbedded) { + if (isNativeWechatMiniProgram()) { + try { + const opened = await openExternalNavigation(validation.value) + return opened + ? { status: 'opened' } + : { status: 'failed', reason: 'navigation-failed' } + } catch (error) { + console.error('原生小程序地图能力调用失败:', error) uni.showToast({ - title: '请在小程序内打开地图', + title: '无法打开微信地图', icon: 'none' }) + return { status: 'failed', reason: 'navigation-failed' } } - return false + } + + if (!isEmbeddedInWechatMiniProgram() && !isWechatBrowser()) { + return { status: 'not-embedded' } + } + + const miniProgram = await waitForWechatMiniProgramBridge() + if (!miniProgram?.navigateTo) { + if (isEmbeddedInWechatMiniProgram()) { + uni.showToast({ + title: '小程序地图桥接加载失败', + icon: 'none' + }) + return { status: 'failed', reason: 'bridge-unavailable' } + } + return { status: 'not-embedded' } } const isMiniProgramWebView = await resolveMiniProgramWebViewEnvironment(miniProgram) - if (!isMiniProgramWebView) return false + if (!isMiniProgramWebView) { + if (isEmbeddedInWechatMiniProgram()) { + uni.showToast({ + title: '当前页面未连接宿主小程序', + icon: 'none' + }) + return { status: 'failed', reason: 'not-in-mini-program' } + } + return { status: 'not-embedded' } + } - return navigateToMiniProgramLocationPage(miniProgram, target) + return navigateToMiniProgramLocationPage(miniProgram, validation.value) +} + +export const openWechatMiniProgramLocation = ( + target: WechatMiniProgramLocationTarget +) => { + if (miniProgramNavigationPromise) return miniProgramNavigationPromise + + miniProgramNavigationPromise = openWechatMiniProgramLocationOnce(target) + .finally(() => { + miniProgramNavigationPromise = null + }) + return miniProgramNavigationPromise } diff --git a/src/services/WechatOpenLocationService.ts b/src/services/WechatOpenLocationService.ts new file mode 100644 index 0000000..d2276ea --- /dev/null +++ b/src/services/WechatOpenLocationService.ts @@ -0,0 +1,40 @@ +import type { WechatOpenLocationTarget } from '@/utils/wechatOpenLocationProtocol' + +export type WechatOpenLocationResult = + | { status: 'opened' } + | { status: 'failed'; reason: 'api-failed' | 'timeout' } + +const OPEN_LOCATION_TIMEOUT = 5000 + +export const openWechatHostLocation = ( + target: WechatOpenLocationTarget +) => new Promise((resolve) => { + let settled = false + const finish = (result: WechatOpenLocationResult) => { + if (settled) return + settled = true + clearTimeout(timeout) + resolve(result) + } + const timeout = setTimeout(() => { + finish({ status: 'failed', reason: 'timeout' }) + }, OPEN_LOCATION_TIMEOUT) + + try { + uni.openLocation({ + latitude: target.latitude, + longitude: target.longitude, + name: target.name, + address: target.address || '', + scale: 18, + success: () => finish({ status: 'opened' }), + fail: (error) => { + console.error('打开地图位置失败:', error) + finish({ status: 'failed', reason: 'api-failed' }) + } + }) + } catch (error) { + console.error('调用微信地图能力失败:', error) + finish({ status: 'failed', reason: 'api-failed' }) + } +}) diff --git a/src/utils/wechatOpenLocationProtocol.ts b/src/utils/wechatOpenLocationProtocol.ts new file mode 100644 index 0000000..c87c839 --- /dev/null +++ b/src/utils/wechatOpenLocationProtocol.ts @@ -0,0 +1,93 @@ +export const WECHAT_OPEN_LOCATION_PAGE_PATH = '/pages/open-location/index' + +export interface WechatOpenLocationTarget { + latitude: number + longitude: number + name: string + address?: string +} + +export interface WechatOpenLocationPageOptions { + latitude?: unknown + longitude?: unknown + name?: unknown + address?: unknown +} + +export type WechatOpenLocationValidationResult = + | { ok: true; value: WechatOpenLocationTarget } + | { ok: false; message: string } + +const MAX_NAME_LENGTH = 100 +const MAX_ADDRESS_LENGTH = 300 + +const firstValue = (value: unknown) => ( + Array.isArray(value) ? value[0] : value +) + +const normalizeText = (value: unknown) => { + const first = firstValue(value) + return typeof first === 'string' ? first.trim() : '' +} + +const normalizeCoordinate = (value: unknown) => { + const first = firstValue(value) + if (typeof first === 'string' && !first.trim()) return null + if (typeof first !== 'string' && typeof first !== 'number') return null + + const coordinate = Number(first) + return Number.isFinite(coordinate) ? coordinate : null +} + +export const validateWechatOpenLocationTarget = ( + target: WechatOpenLocationPageOptions +): WechatOpenLocationValidationResult => { + const latitude = normalizeCoordinate(target.latitude) + const longitude = normalizeCoordinate(target.longitude) + const name = normalizeText(target.name) + const address = normalizeText(target.address) + + if (latitude === null || longitude === null) { + return { ok: false, message: '地图坐标缺失或格式错误' } + } + if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) { + return { ok: false, message: '地图坐标超出有效范围' } + } + if (!name) { + return { ok: false, message: '目的地名称不能为空' } + } + if (name.length > MAX_NAME_LENGTH || address.length > MAX_ADDRESS_LENGTH) { + return { ok: false, message: '目的地名称或地址过长' } + } + + return { + ok: true, + value: { + latitude, + longitude, + name, + address + } + } +} + +export const buildWechatOpenLocationPageUrl = ( + target: WechatOpenLocationPageOptions +) => { + const result = validateWechatOpenLocationTarget(target) + if (!result.ok) throw new TypeError(result.message) + + const params = new URLSearchParams() + params.set('latitude', String(result.value.latitude)) + params.set('longitude', String(result.value.longitude)) + params.set('name', result.value.name) + params.set('address', result.value.address || '') + + // 微信页面路由对加号的解码行为不统一,空格固定使用百分号编码。 + const query = params.toString().replace(/\+/g, '%20') + return `${WECHAT_OPEN_LOCATION_PAGE_PATH}?${query}` +} + +export const parseWechatOpenLocationPageOptions = ( + options: WechatOpenLocationPageOptions = {} +) => validateWechatOpenLocationTarget(options) diff --git a/tests/unit/ThirdPartyMapSearchService.spec.ts b/tests/unit/ThirdPartyMapSearchService.spec.ts new file mode 100644 index 0000000..a981d35 --- /dev/null +++ b/tests/unit/ThirdPartyMapSearchService.spec.ts @@ -0,0 +1,231 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/utils/hostEnvironment', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + isNativeWechatMiniProgram: () => false + } +}) + +import { + buildThirdPartyMapSearchUri, + openWechatMiniProgramLocation +} from '@/services/ThirdPartyMapSearchService' + +const target = { + latitude: 22.604321, + longitude: 114.058765, + name: '深圳自然博物馆', + address: '深圳市龙华区民治街道' +} + +const createWindow = (overrides: Record = {}) => ({ + location: { search: '', hash: '' }, + navigator: { userAgent: 'Mozilla/5.0 MicroMessenger/8.0.50' }, + ...overrides +}) + +beforeEach(() => { + vi.useFakeTimers() + vi.stubGlobal('document', undefined) +}) + +afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() +}) + +describe('微信小程序 web-view 地图桥接', () => { + it('桥接延迟注入时快速调用两次也只跳转一次', async () => { + const navigateTo = vi.fn((options: { success?: () => void }) => options.success?.()) + const testWindow = createWindow() + vi.stubGlobal('window', testWindow) + vi.stubGlobal('uni', { showToast: vi.fn() }) + + setTimeout(() => { + Object.assign(testWindow, { + __wxjs_environment: 'miniprogram', + wx: { miniProgram: { navigateTo } } + }) + }, 100) + + const first = openWechatMiniProgramLocation(target) + const second = openWechatMiniProgramLocation(target) + await vi.advanceTimersByTimeAsync(150) + + await expect(first).resolves.toEqual({ status: 'opened' }) + await expect(second).resolves.toEqual({ status: 'opened' }) + expect(navigateTo).toHaveBeenCalledTimes(1) + + const url = navigateTo.mock.calls[0]?.[0].url + const params = new URLSearchParams(url.split('?')[1]) + expect(Object.fromEntries(params)).toEqual({ + latitude: String(target.latitude), + longitude: String(target.longitude), + name: target.name, + address: target.address + }) + }) + + it('普通浏览器返回 H5 分支并保留第三方地图 URI', async () => { + const showToast = vi.fn() + vi.stubGlobal('window', createWindow({ + navigator: { userAgent: 'Mozilla/5.0 Chrome/126.0' } + })) + vi.stubGlobal('uni', { showToast }) + + await expect(openWechatMiniProgramLocation(target)).resolves.toEqual({ status: 'not-embedded' }) + expect(showToast).not.toHaveBeenCalled() + expect(buildThirdPartyMapSearchUri('amap', { + keyword: '深圳自然博物馆', + region: '深圳市' + })).toContain('https://uri.amap.com/search?') + }) + + it('显式宿主环境缺少桥接时显示错误提示', async () => { + const showToast = vi.fn() + vi.stubGlobal('window', createWindow({ + location: { search: '?embedded=weapp', hash: '' } + })) + vi.stubGlobal('uni', { showToast }) + + const result = openWechatMiniProgramLocation(target) + await vi.advanceTimersByTimeAsync(2600) + + await expect(result).resolves.toEqual({ status: 'failed', reason: 'bridge-unavailable' }) + expect(showToast).toHaveBeenCalledWith({ + title: '小程序地图桥接加载失败', + icon: 'none' + }) + }) + + it('微信 SDK 首次加载失败后再次点击会重新创建脚本', async () => { + const showToast = vi.fn() + let currentScript: Record | null = null + const appendChild = vi.fn((script: Record) => { + currentScript = script + setTimeout(() => (script.onerror as (() => void) | undefined)?.(), 0) + }) + vi.stubGlobal('window', createWindow({ + location: { search: '?embedded=weapp', hash: '' } + })) + vi.stubGlobal('document', { + querySelector: () => currentScript, + createElement: () => { + const script: Record = { dataset: {} } + script.remove = () => { + if (currentScript === script) currentScript = null + } + return script + }, + head: { appendChild } + }) + vi.stubGlobal('uni', { showToast }) + + const first = openWechatMiniProgramLocation(target) + await vi.advanceTimersByTimeAsync(2600) + await expect(first).resolves.toEqual({ status: 'failed', reason: 'bridge-unavailable' }) + + const second = openWechatMiniProgramLocation(target) + await vi.advanceTimersByTimeAsync(2600) + await expect(second).resolves.toEqual({ status: 'failed', reason: 'bridge-unavailable' }) + expect(appendChild).toHaveBeenCalledTimes(2) + }) + + it('宿主页跳转失败时给出缺页提示', async () => { + const showToast = vi.fn() + const navigateTo = vi.fn((options: { fail?: (error: unknown) => void }) => { + options.fail?.({ errMsg: 'navigateTo:fail page not found' }) + }) + vi.stubGlobal('window', createWindow({ + location: { search: '?embedded=weapp', hash: '' }, + wx: { miniProgram: { navigateTo } } + })) + vi.stubGlobal('uni', { showToast }) + + await expect(openWechatMiniProgramLocation(target)).resolves.toEqual({ + status: 'failed', + reason: 'navigation-failed' + }) + expect(showToast).toHaveBeenCalledWith({ + title: '宿主小程序未配置地图页', + icon: 'none' + }) + }) + + it('宿主页没有回调时超时返回并显示提示', async () => { + const showToast = vi.fn() + let navigateOptions: { fail?: (error: unknown) => void } | undefined + vi.stubGlobal('window', createWindow({ + location: { search: '?embedded=weapp', hash: '' }, + wx: { + miniProgram: { + navigateTo: vi.fn((options: { fail?: (error: unknown) => void }) => { + navigateOptions = options + }) + } + } + })) + vi.stubGlobal('uni', { showToast }) + + const result = openWechatMiniProgramLocation(target) + await vi.advanceTimersByTimeAsync(3100) + + await expect(result).resolves.toEqual({ status: 'failed', reason: 'navigation-failed' }) + expect(showToast).toHaveBeenCalledWith({ + title: '宿主地图页响应超时', + icon: 'none' + }) + navigateOptions?.fail?.({ errMsg: 'navigateTo:fail page not found' }) + expect(showToast).toHaveBeenCalledTimes(1) + }) + + it('非缺页类宿主跳转失败使用通用提示', async () => { + const showToast = vi.fn() + vi.stubGlobal('window', createWindow({ + location: { search: '?embedded=weapp', hash: '' }, + wx: { + miniProgram: { + navigateTo: vi.fn((options: { fail?: (error: unknown) => void }) => { + options.fail?.({ errMsg: 'navigateTo:fail page stack full' }) + }) + } + } + })) + vi.stubGlobal('uni', { showToast }) + + await expect(openWechatMiniProgramLocation(target)).resolves.toEqual({ + status: 'failed', + reason: 'navigation-failed' + }) + expect(showToast).toHaveBeenCalledWith({ + title: '宿主地图页跳转失败', + icon: 'none' + }) + }) + + it('宿主桥接同步抛错时显示调用失败提示', async () => { + const showToast = vi.fn() + vi.stubGlobal('window', createWindow({ + location: { search: '?embedded=weapp', hash: '' }, + wx: { + miniProgram: { + navigateTo: vi.fn(() => { + throw new Error('bridge unavailable') + }) + } + } + })) + vi.stubGlobal('uni', { showToast }) + + await expect(openWechatMiniProgramLocation(target)).resolves.toEqual({ + status: 'failed', + reason: 'navigation-failed' + }) + expect(showToast).toHaveBeenCalledWith({ + title: '宿主地图桥接调用失败', + icon: 'none' + }) + }) +}) diff --git a/tests/unit/WechatOpenLocationService.spec.ts b/tests/unit/WechatOpenLocationService.spec.ts new file mode 100644 index 0000000..71346bf --- /dev/null +++ b/tests/unit/WechatOpenLocationService.spec.ts @@ -0,0 +1,53 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { openWechatHostLocation } from '@/services/WechatOpenLocationService' + +const target = { + latitude: 22.604321, + longitude: 114.058765, + name: '深圳自然博物馆', + address: '深圳市龙华区民治街道' +} + +beforeEach(() => { + vi.useFakeTimers() +}) + +afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() +}) + +describe('宿主微信地图能力', () => { + it('向 uni.openLocation 透传完整位置参数', async () => { + const openLocation = vi.fn((options: { success?: () => void }) => options.success?.()) + vi.stubGlobal('uni', { openLocation }) + + await expect(openWechatHostLocation(target)).resolves.toEqual({ status: 'opened' }) + expect(openLocation).toHaveBeenCalledWith(expect.objectContaining({ + ...target, + scale: 18 + })) + }) + + it('宿主能力无回调时按超时失败并释放调用', async () => { + vi.stubGlobal('uni', { openLocation: vi.fn() }) + + const result = openWechatHostLocation(target) + await vi.advanceTimersByTimeAsync(5100) + + await expect(result).resolves.toEqual({ status: 'failed', reason: 'timeout' }) + }) + + it('宿主能力同步抛错时返回可处理的失败结果', async () => { + vi.stubGlobal('uni', { + openLocation: vi.fn(() => { + throw new Error('openLocation unavailable') + }) + }) + + await expect(openWechatHostLocation(target)).resolves.toEqual({ + status: 'failed', + reason: 'api-failed' + }) + }) +}) diff --git a/tests/unit/hostEnvironment.spec.ts b/tests/unit/hostEnvironment.spec.ts new file mode 100644 index 0000000..3a82f05 --- /dev/null +++ b/tests/unit/hostEnvironment.spec.ts @@ -0,0 +1,49 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment' + +const createWindow = (overrides: Record = {}) => ({ + location: { search: '', hash: '' }, + navigator: { userAgent: '' }, + ...overrides +}) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('微信小程序宿主环境识别', () => { + it('同一模块可以识别延迟注入的微信环境标识', () => { + const testWindow = createWindow() + vi.stubGlobal('window', testWindow) + + expect(isEmbeddedInWechatMiniProgram()).toBe(false) + Object.assign(testWindow, { __wxjs_environment: 'miniprogram' }) + expect(isEmbeddedInWechatMiniProgram()).toBe(true) + }) + + it('支持宿主在普通查询或 hash 查询中显式声明环境', () => { + vi.stubGlobal('window', createWindow({ + location: { + search: '?embedded=weapp', + hash: '#/pages/index/index?tab=guide' + } + })) + expect(isEmbeddedInWechatMiniProgram()).toBe(true) + + vi.stubGlobal('window', createWindow({ + location: { + search: '', + hash: '#/pages/index/index?tab=guide&embedded=wechat-mini-program' + } + })) + expect(isEmbeddedInWechatMiniProgram()).toBe(true) + }) + + it('普通微信 H5 不会被误判为小程序 web-view', () => { + vi.stubGlobal('window', createWindow({ + navigator: { userAgent: 'Mozilla/5.0 MicroMessenger/8.0.50' } + })) + + expect(isEmbeddedInWechatMiniProgram()).toBe(false) + }) +}) diff --git a/tests/unit/wechatOpenLocationProtocol.spec.ts b/tests/unit/wechatOpenLocationProtocol.spec.ts new file mode 100644 index 0000000..b8fee15 --- /dev/null +++ b/tests/unit/wechatOpenLocationProtocol.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' +import { + buildWechatOpenLocationPageUrl, + parseWechatOpenLocationPageOptions, + validateWechatOpenLocationTarget, + WECHAT_OPEN_LOCATION_PAGE_PATH +} from '@/utils/wechatOpenLocationProtocol' + +describe('微信宿主地图参数协议', () => { + it('中文及特殊字符编码后可以完整还原', () => { + const source = { + latitude: 22.604321, + longitude: 114.058765, + name: '深圳自然博物馆 A&B #1', + address: '龙华区 100% 路口?入口=东&层=1' + } + const url = buildWechatOpenLocationPageUrl(source) + const [path, query = ''] = url.split('?') + const params = Object.fromEntries(new URLSearchParams(query)) + + expect(path).toBe(WECHAT_OPEN_LOCATION_PAGE_PATH) + expect(parseWechatOpenLocationPageOptions(params)).toEqual({ + ok: true, + value: source + }) + }) + + it.each([ + [{ latitude: '', longitude: 114, name: '博物馆' }, '地图坐标缺失或格式错误'], + [{ latitude: Number.NaN, longitude: 114, name: '博物馆' }, '地图坐标缺失或格式错误'], + [{ latitude: 91, longitude: 114, name: '博物馆' }, '地图坐标超出有效范围'], + [{ latitude: 22, longitude: 181, name: '博物馆' }, '地图坐标超出有效范围'], + [{ latitude: 22, longitude: 114, name: ' ' }, '目的地名称不能为空'] + ])('拒绝非法参数 %#', (target, message) => { + expect(validateWechatOpenLocationTarget(target)).toEqual({ ok: false, message }) + expect(() => buildWechatOpenLocationPageUrl(target)).toThrow(message) + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..f928c38 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,16 @@ +import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + test: { + environment: 'node', + include: ['tests/unit/**/*.spec.ts'], + clearMocks: true, + restoreMocks: true + } +})