357
tests/unit/PoiSearchPanel.spec.ts
Normal file
357
tests/unit/PoiSearchPanel.spec.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import PoiSearchPanel from '@/components/search/PoiSearchPanel.vue'
|
||||
import type { MuseumFloor, MuseumPoi } from '@/domain/museum'
|
||||
|
||||
const guideMocks = vi.hoisted(() => ({
|
||||
getFloors: vi.fn(),
|
||||
getInitialSearchSpacePoints: vi.fn(),
|
||||
searchPois: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/usecases/guideUseCase', () => ({
|
||||
guideUseCase: guideMocks
|
||||
}))
|
||||
|
||||
const floors: MuseumFloor[] = [
|
||||
{ id: 'floor-1', label: '1F', order: 1 },
|
||||
{ id: 'floor-2', label: '2F', order: 2 }
|
||||
]
|
||||
|
||||
const createPoi = (
|
||||
id: string,
|
||||
name: string,
|
||||
floorId: string,
|
||||
floorLabel: string,
|
||||
iconType: string,
|
||||
positionGltf?: [number, number, number]
|
||||
): MuseumPoi => ({
|
||||
id,
|
||||
name,
|
||||
floorId,
|
||||
floorLabel,
|
||||
primaryCategory: {
|
||||
id: iconType,
|
||||
label: name.includes('卫生间') ? '卫生间' : name.includes('母婴') ? '母婴室' : '展厅',
|
||||
iconType
|
||||
},
|
||||
categories: [],
|
||||
positionGltf,
|
||||
accessible: true
|
||||
})
|
||||
|
||||
const poiPool: MuseumPoi[] = [
|
||||
createPoi('hall-1', '地球厅', 'floor-1', '1F', 'exhibition_hall', [1, 0, 1]),
|
||||
createPoi('restroom-1', '卫生间 A', 'floor-1', '1F', 'toilet', [2, 0, 2]),
|
||||
createPoi('restroom-missing-position', '卫生间 B', 'floor-1', '1F', 'toilet'),
|
||||
createPoi('restroom-2', '卫生间 C', 'floor-2', '2F', 'toilet', [3, 0, 3]),
|
||||
createPoi('nursing-2', '母婴室', 'floor-2', '2F', 'mother_baby_room', [4, 0, 4])
|
||||
]
|
||||
|
||||
const flushMountedSearch = async () => {
|
||||
await flushPromises()
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
const createDeferred = <T>() => {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void
|
||||
let reject!: (reason?: unknown) => void
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise
|
||||
reject = rejectPromise
|
||||
})
|
||||
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
guideMocks.getFloors.mockResolvedValue(floors)
|
||||
guideMocks.getInitialSearchSpacePoints.mockResolvedValue(poiPool)
|
||||
guideMocks.searchPois.mockImplementation(async (keyword = '') => (
|
||||
keyword
|
||||
? poiPool.filter((poi) => poi.name.includes(keyword))
|
||||
: poiPool
|
||||
))
|
||||
vi.stubGlobal('uni', {
|
||||
hideKeyboard: vi.fn(),
|
||||
navigateTo: vi.fn(),
|
||||
showToast: vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.documentElement.className = ''
|
||||
document.body.className = ''
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('POI 搜索面板实际渲染与状态闭环', () => {
|
||||
it('首页显示六类统一入口,搜索页显示完整十一类入口', async () => {
|
||||
const homeWrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
const homeCategories = homeWrapper.findAll('.home-category-chip')
|
||||
expect(homeCategories).toHaveLength(6)
|
||||
expect(homeCategories.map((item) => item.text())).toEqual([
|
||||
'展厅', '卫生间', '母婴室', '电梯', '楼梯', '扶梯'
|
||||
])
|
||||
homeCategories.forEach((item) => {
|
||||
expect(item.find('.line-icon').exists()).toBe(true)
|
||||
expect(item.find('.home-category-label').exists()).toBe(true)
|
||||
})
|
||||
homeWrapper.unmount()
|
||||
|
||||
const pageWrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
expect(pageWrapper.findAll('.category-item')).toHaveLength(11)
|
||||
expect(pageWrapper.text()).toContain('影院')
|
||||
expect(pageWrapper.text()).toContain('服务中心')
|
||||
expect(pageWrapper.text()).toContain('文创')
|
||||
expect(pageWrapper.text()).not.toContain('服务设施')
|
||||
pageWrapper.unmount()
|
||||
})
|
||||
|
||||
it('首页分类只展示当前楼层结果,并上报可定位点位的精确 marker ID', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.get('[data-testid="poi-home-category-results"]').text()).toContain('当前楼层 1F · 2 个点位')
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-1"]').exists()).toBe(true)
|
||||
const missingPositionRow = wrapper.get('[data-testid="poi-result-restroom-missing-position"]')
|
||||
expect(missingPositionRow.classes()).toContain('disabled')
|
||||
expect(missingPositionRow.text()).toContain('暂无地图坐标')
|
||||
|
||||
const resultStates = wrapper.emitted('results-change') || []
|
||||
expect(resultStates.at(-1)?.[0]).toMatchObject({
|
||||
active: true,
|
||||
categoryId: 'restroom',
|
||||
floorId: 'floor-1',
|
||||
floorLabel: '1F',
|
||||
resultCount: 2,
|
||||
floorResultCount: 2,
|
||||
visiblePoiIds: ['restroom-1']
|
||||
})
|
||||
|
||||
await missingPositionRow.trigger('tap')
|
||||
expect(wrapper.emitted('resultTap')).toBeUndefined()
|
||||
expect(uni.showToast).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: '该点位缺少地图坐标,暂无法定位'
|
||||
}))
|
||||
|
||||
await wrapper.get('[data-testid="poi-result-restroom-1"]').trigger('tap')
|
||||
expect(wrapper.emitted('resultTap')?.at(-1)?.[1]).toMatchObject({
|
||||
categoryId: 'restroom',
|
||||
resultCount: 2,
|
||||
visiblePoiIds: ['restroom-1']
|
||||
})
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('首页分类跟随当前地图楼层更新结果与 marker ID,并对无结果给出反馈', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
await wrapper.setProps({ currentFloorId: 'floor-2', currentFloorLabel: '2F' })
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.get('[data-testid="poi-home-category-results"]').text()).toContain('当前楼层 2F · 1 个点位')
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-2"]').exists()).toBe(true)
|
||||
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
|
||||
active: true,
|
||||
floorId: 'floor-2',
|
||||
visiblePoiIds: ['restroom-2']
|
||||
})
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-cancel"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
await wrapper.get('[data-testid="poi-category-nursing-room"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('[data-testid="poi-result-nursing-2"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.setProps({ currentFloorId: 'floor-1', currentFloorLabel: '1F' })
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.get('[data-testid="poi-empty"]').text()).toContain('当前楼层暂无匹配点位')
|
||||
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
|
||||
active: true,
|
||||
visiblePoiIds: []
|
||||
})
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('独立搜索页进入详情时传递关键词、分类、楼层、结果数和列表位置', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
const floor2Tab = wrapper.findAll('.floor-tab').find((item) => item.text() === '2F')
|
||||
expect(floor2Tab).toBeDefined()
|
||||
await floor2Tab!.trigger('tap')
|
||||
await flushMountedSearch()
|
||||
wrapper.get('[data-testid="poi-result-list"]').element.scrollTop = 128
|
||||
await wrapper.get('[data-testid="poi-result-restroom-2"]').trigger('tap')
|
||||
|
||||
expect(uni.navigateTo).toHaveBeenCalledTimes(1)
|
||||
const url = vi.mocked(uni.navigateTo).mock.calls[0][0].url
|
||||
const query = new URLSearchParams(url.split('?')[1])
|
||||
expect(query.get('source')).toBe('search')
|
||||
expect(query.get('searchOrigin')).toBe('page')
|
||||
expect(query.get('searchKeyword')).toBe('卫生间')
|
||||
expect(query.get('searchCategoryId')).toBe('restroom')
|
||||
expect(query.get('searchFloorId')).toBe('floor-2')
|
||||
expect(query.get('resultCount')).toBe('3')
|
||||
expect(query.get('floorResultCount')).toBe('1')
|
||||
expect(query.get('visiblePoiIds')).toBe('restroom-2')
|
||||
expect(query.get('listScrollTop')).toBe('128')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('页面重新显示时恢复原结果列表位置', async () => {
|
||||
const wrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
const resultList = wrapper.get('[data-testid="poi-result-list"]')
|
||||
resultList.element.scrollTop = 128
|
||||
await wrapper.get('[data-testid="poi-result-restroom-1"]').trigger('tap')
|
||||
resultList.element.scrollTop = 0
|
||||
|
||||
await (wrapper.vm as unknown as {
|
||||
restoreResultListScroll: () => Promise<void>
|
||||
}).restoreResultListScroll()
|
||||
|
||||
expect(resultList.element.scrollTop).toBe(128)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('部分数据不会固化,分类点击会重新读取并恢复结果', async () => {
|
||||
guideMocks.getInitialSearchSpacePoints
|
||||
.mockResolvedValueOnce([poiPool[0]])
|
||||
.mockResolvedValueOnce(poiPool)
|
||||
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(guideMocks.getInitialSearchSpacePoints).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-1"]').exists()).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('旧的部分结果晚到时不会覆盖最新完整缓存', async () => {
|
||||
const oldRequest = createDeferred<MuseumPoi[]>()
|
||||
const latestRequest = createDeferred<MuseumPoi[]>()
|
||||
guideMocks.getInitialSearchSpacePoints
|
||||
.mockImplementationOnce(() => oldRequest.promise)
|
||||
.mockImplementationOnce(() => latestRequest.promise)
|
||||
|
||||
const wrapper = mount(PoiSearchPanel, {
|
||||
props: {
|
||||
variant: 'home',
|
||||
currentFloorId: 'floor-1',
|
||||
currentFloorLabel: '1F'
|
||||
}
|
||||
})
|
||||
await flushMountedSearch()
|
||||
expect(guideMocks.getInitialSearchSpacePoints).toHaveBeenCalledTimes(1)
|
||||
|
||||
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
|
||||
await flushPromises()
|
||||
expect(guideMocks.getInitialSearchSpacePoints).toHaveBeenCalledTimes(2)
|
||||
|
||||
latestRequest.resolve(poiPool)
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-1"]').exists()).toBe(true)
|
||||
|
||||
oldRequest.resolve([poiPool[0]])
|
||||
await flushMountedSearch()
|
||||
await wrapper.get('[data-testid="poi-category-cancel"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
await wrapper.get('.search-box').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.find('[data-testid="poi-result-restroom-1"]').exists()).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('服务中心只展示真实服务台,不把普通服务空间误归类', async () => {
|
||||
const serviceSpace = createPoi(
|
||||
'space-service-lounge',
|
||||
'贵宾休息室',
|
||||
'floor-1',
|
||||
'1F',
|
||||
'service_space',
|
||||
[5, 0, 5]
|
||||
)
|
||||
const serviceDesk = createPoi(
|
||||
'poi-service-desk',
|
||||
'咨询服务台',
|
||||
'floor-1',
|
||||
'1F',
|
||||
'service_desk',
|
||||
[6, 0, 6]
|
||||
)
|
||||
guideMocks.getInitialSearchSpacePoints.mockResolvedValue([serviceSpace, serviceDesk])
|
||||
|
||||
const wrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
await wrapper.get('[data-testid="poi-category-service-center"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.find('[data-testid="poi-result-poi-service-desk"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="poi-result-space-service-lounge"]').exists()).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('弱网失败显示可重试状态,恢复后重新渲染结果', async () => {
|
||||
guideMocks.getInitialSearchSpacePoints
|
||||
.mockRejectedValueOnce(new Error('network down'))
|
||||
.mockResolvedValueOnce(poiPool)
|
||||
|
||||
const wrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
|
||||
await flushMountedSearch()
|
||||
|
||||
expect(wrapper.get('[data-testid="poi-empty"]').text()).toContain('点位加载失败')
|
||||
expect(wrapper.find('[data-testid="poi-retry"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('[data-testid="poi-retry"]').trigger('tap')
|
||||
await flushMountedSearch()
|
||||
expect(wrapper.find('[data-testid="poi-retry"]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-testid="poi-result-hall-1"]').exists()).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user