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