完善馆内点位展示与微信地图桥接
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-19 16:01:32 +08:00
parent 127461dd60
commit d199b4602f
10 changed files with 780 additions and 334 deletions

View File

@@ -0,0 +1,94 @@
import { expect, test, type Page } from '@playwright/test'
interface PoiCandidate {
poiId: string
floorId: string
name: string
primaryCategory: string
positionGltf: [number, number, number]
}
interface GuideDiagnostics {
isInitialModelReady: () => boolean
getFloors: () => Array<{ floorId: string }>
getReport: () => { floorId: string; activeFocusPoiId: string }
resetToViewBaseline: (options: { view: 'floor'; floorId: string; reason: 'floor-reset' }) => Promise<unknown>
getFloorPois: (floorId: string) => Promise<PoiCandidate[]>
focusTargetPoi: (request: PoiCandidate & { requestId: string }) => Promise<unknown>
clearTargetFocus: () => void
}
const openGuide = async (page: Page) => {
await page.goto('/')
await page.waitForURL(/tab=guide/, { timeout: 30_000 })
await expect.poll(async () => page.evaluate(() => (
(window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
.__GUIDE_3D_VISUAL_STABILITY__?.isInitialModelReady() || false
)), { timeout: 180_000 }).toBe(true)
}
const resetToActiveFloor = async (page: Page) => page.evaluate(async () => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
.__GUIDE_3D_VISUAL_STABILITY__
if (!api) throw new Error('ThreeMap diagnostics unavailable')
await api.resetToViewBaseline({ view: 'floor', floorId: api.getReport().floorId, reason: 'floor-reset' })
return api.getReport().floorId
})
for (const deviceScaleFactor of [1, 3]) {
test.describe(`POI DOM labels at DPR ${deviceScaleFactor}`, () => {
test.use({ viewport: { width: 390, height: 844 }, deviceScaleFactor })
test('renders measured native labels and clears focused labels without residue', async ({ page }, testInfo) => {
await openGuide(page)
const floorId = await resetToActiveFloor(page)
const labels = page.locator('[data-poi-label-kind="ambient"]:visible')
await expect(labels.first()).toBeVisible({ timeout: 30_000 })
const hall = page.locator('[data-poi-label-kind="ambient"].three-poi-dom-label--hall:visible').first()
const facility = page.locator('[data-poi-label-kind="ambient"].three-poi-dom-label--service').first()
await expect(hall).toBeVisible()
for (const locator of [hall, facility]) {
await expect(locator).toHaveCSS('pointer-events', 'none')
const fontSize = await locator.evaluate((element) => Number.parseFloat(getComputedStyle(element).fontSize))
expect(fontSize).toBeGreaterThanOrEqual(locator === hall ? 14 : 13)
}
for (const locator of [hall]) {
const box = await locator.boundingBox()
expect(box).not.toBeNull()
expect(box!.x).toBeGreaterThanOrEqual(0)
expect(box!.y).toBeGreaterThanOrEqual(0)
expect(box!.x + box!.width).toBeLessThanOrEqual(390)
expect(box!.y + box!.height).toBeLessThanOrEqual(844)
}
const poiId = await facility.getAttribute('data-poi-id')
expect(poiId).toBeTruthy()
await page.evaluate(async ({ floorId: targetFloorId, targetPoiId }) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
.__GUIDE_3D_VISUAL_STABILITY__
if (!api) throw new Error('ThreeMap diagnostics unavailable')
const poi = (await api.getFloorPois(targetFloorId)).find((candidate) => candidate.poiId === targetPoiId)
if (!poi) throw new Error(`missing POI ${targetPoiId}`)
await api.focusTargetPoi({ ...poi, requestId: `poi-dom-label-${targetPoiId}` })
}, { floorId, targetPoiId: poiId! })
const focus = page.locator('[data-poi-label-kind="focus"][data-focus-label="active"]')
await expect(focus).toBeVisible({ timeout: 20_000 })
await expect(focus).toHaveCSS('pointer-events', 'none')
await expect(focus.locator('.three-poi-dom-label__title')).toHaveCSS('font-size', '16px')
await expect(focus.locator('.three-poi-dom-label__meta')).toHaveCSS('font-size', '13px')
await page.screenshot({ path: testInfo.outputPath(`poi-dom-labels-dpr-${deviceScaleFactor}-focus.png`) })
await page.evaluate(() => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics })
.__GUIDE_3D_VISUAL_STABILITY__
api?.clearTargetFocus()
})
await expect(focus).toHaveCount(0)
await page.screenshot({ path: testInfo.outputPath(`poi-dom-labels-dpr-${deviceScaleFactor}-ambient.png`) })
})
})
}

View File

@@ -4,14 +4,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import ArrivalPanel from '@/components/navigation/ArrivalPanel.vue'
import { ARRIVAL_TARGETS } from '@/data/arrivalSearchTargets'
import { openExternalNavigation } from '@/services/MapNavigationService'
import { openWechatMiniProgramLocation } from '@/services/WechatMiniProgramBridgeService'
const selectedTarget = ARRIVAL_TARGETS[0]
vi.mock('@/services/MapNavigationService', () => ({ openExternalNavigation: vi.fn() }))
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({ openWechatMiniProgramLocation: vi.fn() }))
const selectedTarget = ARRIVAL_TARGETS[1]
const defaultUserAgent = window.navigator.userAgent
const mountArrivalPanel = () => mount(ArrivalPanel, {
props: {
visible: true,
activeType: 'bus',
activeType: selectedTarget.type,
targets: [selectedTarget],
selectedTargetId: selectedTarget.id,
selectedTarget
@@ -19,72 +23,62 @@ const mountArrivalPanel = () => mount(ArrivalPanel, {
})
beforeEach(() => {
vi.mocked(openExternalNavigation).mockResolvedValue(true)
vi.mocked(openWechatMiniProgramLocation).mockResolvedValue({ status: 'not-embedded' })
vi.stubGlobal('uni', { showToast: vi.fn(), showActionSheet: vi.fn() })
window.history.replaceState({}, '', '/#/pages/index/index?tab=guide')
Object.defineProperty(window.navigator, 'userAgent', {
configurable: true,
value: 'Mozilla/5.0 MicroMessenger/8.0.50 miniProgram'
})
Object.assign(window, {
__wxjs_environment: 'miniprogram',
wx: {
miniProgram: {
navigateTo: vi.fn()
}
}
})
vi.stubGlobal('uni', {
showToast: vi.fn(),
showActionSheet: vi.fn()
})
delete (window as Window & { __wxjs_environment?: string }).__wxjs_environment
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: 'Mozilla/5.0 (Linux; Android 14)' })
})
afterEach(() => {
Object.defineProperty(window.navigator, 'userAgent', {
configurable: true,
value: defaultUserAgent
})
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: defaultUserAgent })
delete (window as Window & { __wxjs_environment?: string }).__wxjs_environment
delete (window as Window & { wx?: unknown }).wx
vi.clearAllMocks()
vi.unstubAllGlobals()
})
describe('来馆面板 H5 实际渲染', () => {
it('主按钮显示第三方导航,点击后弹出百度和高德选择框', async () => {
describe('来馆面板 H5 导航分流', () => {
it('嵌入小程序 web-view 时只向宿主页面导航,且完整传递当前选中目标', async () => {
Object.assign(window, { __wxjs_environment: 'miniprogram' })
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: 'Mozilla/5.0 MicroMessenger/8.0.50' })
vi.mocked(openWechatMiniProgramLocation).mockResolvedValue({ status: 'opened' })
const wrapper = mountArrivalPanel()
const showActionSheet = (uni as unknown as {
showActionSheet: ReturnType<typeof vi.fn>
}).showActionSheet
const navigateTo = (
window as Window & {
wx?: { miniProgram?: { navigateTo?: ReturnType<typeof vi.fn> } }
}
).wx?.miniProgram?.navigateTo
expect(wrapper.get('.arrival-primary-text').text()).toBe('第三方导航')
expect(wrapper.text()).not.toContain('打开地图导航')
expect(wrapper.get('.arrival-primary-text').text()).toBe('打开地图导航')
await wrapper.get('.arrival-primary').trigger('tap')
expect(showActionSheet).toHaveBeenCalledWith(expect.objectContaining({
itemList: ['百度地图', '高德地图']
}))
expect(wrapper.find('.provider-sheet').exists()).toBe(false)
expect(navigateTo).not.toHaveBeenCalled()
expect(openWechatMiniProgramLocation).toHaveBeenCalledTimes(1)
expect(openWechatMiniProgramLocation).toHaveBeenCalledWith({
latitude: selectedTarget.latitude,
longitude: selectedTarget.longitude,
name: selectedTarget.title,
address: selectedTarget.subtitle
})
expect(openExternalNavigation).not.toHaveBeenCalled()
expect((uni as unknown as { showActionSheet: ReturnType<typeof vi.fn> }).showActionSheet).not.toHaveBeenCalled()
})
it('诊断信息默认隐藏,仅在 debug-build=1 时显示 H5 构建边界', () => {
it('普通 H5 仅回退至第三方地图,并使用当前选中目标', async () => {
const wrapper = mountArrivalPanel()
expect(wrapper.get('.arrival-primary-text').text()).toBe('第三方导航')
await wrapper.get('.arrival-primary').trigger('tap')
expect(openExternalNavigation).toHaveBeenCalledWith({
latitude: selectedTarget.latitude,
longitude: selectedTarget.longitude,
name: selectedTarget.title,
address: selectedTarget.subtitle
})
})
it('诊断信息默认隐藏,仅在 debug-build=1 时显示构建边界', () => {
const normalWrapper = mountArrivalPanel()
expect(normalWrapper.find('[data-testid="arrival-build-diagnostics"]').exists()).toBe(false)
normalWrapper.unmount()
window.location.hash = '#/pages/index/index?tab=guide&debug-build=1'
const debugWrapper = mountArrivalPanel()
const diagnostics = debugWrapper.get('[data-testid="arrival-build-diagnostics"]')
expect(diagnostics.text()).toContain('构建 IDtest-build')
expect(diagnostics.text()).toContain('Git committest-commit')
expect(diagnostics.text()).toContain('构建平台h5')
expect(diagnostics.text()).toContain('navigationModethird-party-providers')
expect(diagnostics.text()).toContain('location.href')
expect(diagnostics.text()).toContain('UA')
expect(debugWrapper.get('[data-testid="arrival-build-diagnostics"]').text()).toContain('构建平台h5')
})
})

View File

@@ -0,0 +1,87 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const target = { latitude: 22.604321, longitude: 114.058765, name: '停车场 A&B', address: '龙华区 #1' }
const setEmbeddedRuntime = () => Object.assign(window, { __wxjs_environment: 'miniprogram' })
const loadService = async () => {
vi.resetModules()
return import('@/services/WechatMiniProgramBridgeService')
}
beforeEach(() => {
vi.useFakeTimers()
vi.stubGlobal('uni', { showToast: vi.fn() })
delete (window as Window & { wx?: unknown }).wx
delete (window as Window & { __wxjs_environment?: string }).__wxjs_environment
window.history.replaceState({}, '', '/')
})
afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
document.querySelectorAll('script[data-wechat-js-sdk="true"]').forEach((node) => node.remove())
})
describe('微信小程序 web-view 桥接', () => {
it('普通浏览器立即返回 not-embedded不加载 SDK', async () => {
const { openWechatMiniProgramLocation } = await loadService()
await expect(openWechatMiniProgramLocation(target)).resolves.toEqual({ status: 'not-embedded' })
expect(document.querySelector('[data-wechat-js-sdk="true"]')).toBeNull()
})
it('等待延迟注入的 bridge 后导航到编码后的宿主页', async () => {
setEmbeddedRuntime()
const { openWechatMiniProgramLocation } = await loadService()
const result = openWechatMiniProgramLocation(target)
await vi.advanceTimersByTimeAsync(100)
const navigateTo = vi.fn(({ success }) => success())
Object.assign(window, { wx: { miniProgram: { getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: true }), navigateTo } } })
await vi.advanceTimersByTimeAsync(100)
await expect(result).resolves.toEqual({ status: 'opened' })
expect(navigateTo).toHaveBeenCalledWith(expect.objectContaining({
url: expect.stringContaining('/pages/open-location/index?latitude=22.604321&longitude=114.058765&name=%E5%81%9C%E8%BD%A6%E5%9C%BA%20A%26B')
}))
})
it('handles getEnv false, navigate fail, page not found, and callback timeout', async () => {
setEmbeddedRuntime()
const { openWechatMiniProgramLocation } = await loadService()
Object.assign(window, { wx: { miniProgram: { getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: false }), navigateTo: vi.fn() } } })
await expect(openWechatMiniProgramLocation(target)).resolves.toEqual({ status: 'failed', reason: 'not-in-mini-program' })
Object.assign(window, { wx: { miniProgram: { getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: true }) } } })
await expect(openWechatMiniProgramLocation(target)).resolves.toEqual({ status: 'failed', reason: 'bridge-unavailable' })
Object.assign(window, { wx: { miniProgram: { getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: true }), navigateTo: vi.fn(({ fail }) => fail({ errMsg: 'navigateTo:fail permission denied' })) } } })
await expect(openWechatMiniProgramLocation(target)).resolves.toEqual({ status: 'failed', reason: 'navigation-failed' })
const navigateTo = vi.fn(({ fail }) => fail({ errMsg: 'navigateTo:fail page not found' }))
Object.assign(window, { wx: { miniProgram: { getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: true }), navigateTo } } })
await expect(openWechatMiniProgramLocation(target)).resolves.toEqual({ status: 'failed', reason: 'page-not-found' })
Object.assign(window, { wx: { miniProgram: { getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: true }), navigateTo: vi.fn() } } })
const timeoutResult = openWechatMiniProgramLocation(target)
await vi.advanceTimersByTimeAsync(3001)
await expect(timeoutResult).resolves.toEqual({ status: 'failed', reason: 'timeout' })
})
it('deduplicates rapid calls while navigation is in flight', async () => {
setEmbeddedRuntime()
const { openWechatMiniProgramLocation } = await loadService()
let succeed: (() => void) | undefined
const navigateTo = vi.fn(({ success }) => { succeed = success })
Object.assign(window, { wx: { miniProgram: { getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: true }), navigateTo } } })
const first = openWechatMiniProgramLocation(target)
expect(openWechatMiniProgramLocation(target)).toBe(first)
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
expect(navigateTo).toHaveBeenCalledTimes(1)
succeed?.()
await expect(first).resolves.toEqual({ status: 'opened' })
expect(navigateTo).toHaveBeenCalledTimes(1)
})
})

View File

@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import * as THREE from 'three'
import {
domLabelBoundsOverlap,
getDomLabelBounds,
isDomLabelWithinViewport,
projectObjectToDom
} from '@/components/map/poiDomLabels'
describe('POI DOM label projection and collision', () => {
it('projects a world-space anchor to CSS pixels', () => {
const camera = new THREE.PerspectiveCamera(60, 2, 0.1, 100)
camera.position.set(0, 0, 10)
camera.lookAt(0, 0, 0)
camera.updateProjectionMatrix()
camera.updateMatrixWorld()
const anchor = new THREE.Object3D()
anchor.position.set(0, 0, 0)
expect(projectObjectToDom(anchor, camera, 390, 195)).toEqual({ x: 195, y: 97.5 })
})
it('uses measured DOM dimensions for bottom-anchored overlap checks', () => {
const hall = getDomLabelBounds({ x: 120, y: 100 }, { width: 146, height: 38 }, 'bottom')
const facility = getDomLabelBounds({ x: 240, y: 100 }, { width: 88, height: 32 }, 'bottom')
expect(domLabelBoundsOverlap(hall, facility, 6)).toBe(true)
expect(isDomLabelWithinViewport(hall, 390, 844)).toBe(true)
expect(isDomLabelWithinViewport(getDomLabelBounds({ x: 5, y: 10 }, { width: 88, height: 32 }, 'bottom'), 390, 844)).toBe(false)
})
})