修复小程序内来馆地图导航
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-11 17:24:45 +08:00
parent 12c0b5ede2
commit f9fefa11ef
12 changed files with 1161 additions and 146 deletions

View File

@@ -0,0 +1,231 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/utils/hostEnvironment', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/utils/hostEnvironment')>()
return {
...actual,
isNativeWechatMiniProgram: () => false
}
})
import {
buildThirdPartyMapSearchUri,
openWechatMiniProgramLocation
} from '@/services/ThirdPartyMapSearchService'
const target = {
latitude: 22.604321,
longitude: 114.058765,
name: '深圳自然博物馆',
address: '深圳市龙华区民治街道'
}
const createWindow = (overrides: Record<string, unknown> = {}) => ({
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<string, unknown> | null = null
const appendChild = vi.fn((script: Record<string, unknown>) => {
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<string, unknown> = { 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'
})
})
})

View File

@@ -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'
})
})
})

View File

@@ -0,0 +1,49 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
const createWindow = (overrides: Record<string, unknown> = {}) => ({
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)
})
})

View File

@@ -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)
})
})