Files
frontend-miniapp/tests/unit/PoiSearchPanel.spec.ts
lyf 2a274aebcf
Some checks failed
CI / verify (push) Has been cancelled
调整馆内点位搜索状态流转
2026-07-13 23:51:27 +08:00

456 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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(5)
expect(homeCategories.map((item) => item.text())).toEqual([
'行程', '展览', '影院', '洗手间', '餐厅'
])
homeCategories.forEach((item) => {
expect(item.find('.poi-category-icon').exists()).toBe(true)
expect(item.find('.home-category-label').exists()).toBe(true)
})
await homeWrapper.get('.search-box').trigger('tap')
await flushMountedSearch()
const expandedCategories = homeWrapper.findAll('.home-shortcut-grid .category-item')
expect(expandedCategories).toHaveLength(10)
expect(expandedCategories.map((item) => item.text())).toEqual([
'行程', '展览', '影院', '洗手间', '餐厅',
'售票处', '母婴室', '服务中心', '电梯', '扶梯'
])
expect(expandedCategories.slice(0, 5).map((item) => item.text()))
.toEqual(homeCategories.map((item) => item.text()))
homeWrapper.unmount()
const pageWrapper = mount(PoiSearchPanel, { props: { variant: 'page' } })
await flushMountedSearch()
expect(pageWrapper.findAll('.category-item')).toHaveLength(12)
expect(pageWrapper.text()).toContain('影院')
expect(pageWrapper.text()).toContain('服务中心')
expect(pageWrapper.text()).toContain('文创')
expect(pageWrapper.text()).not.toContain('服务设施')
pageWrapper.unmount()
})
it('展开态快捷入口退出全屏并进入当前楼层的分类结果', async () => {
const wrapper = mount(PoiSearchPanel, {
props: {
variant: 'home',
currentFloorId: 'floor-1',
currentFloorLabel: '1F'
}
})
await flushMountedSearch()
await wrapper.get('.search-box').trigger('tap')
await flushMountedSearch()
expect(wrapper.find('.poi-search-content').exists()).toBe(true)
await wrapper.get('[data-testid="poi-category-restroom"]').trigger('tap')
await flushMountedSearch()
expect(wrapper.find('.poi-search-content').exists()).toBe(false)
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(true)
expect(wrapper.get('[data-testid="poi-home-category-results"]').text())
.toContain('当前楼层 1F · 2 个点位')
expect(uni.hideKeyboard).toHaveBeenCalled()
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
active: true,
categoryId: 'restroom',
floorId: 'floor-1',
visiblePoiIds: ['restroom-1']
})
wrapper.unmount()
})
it('关键词确认命中分类时仍保留全屏搜索结果', async () => {
const wrapper = mount(PoiSearchPanel, {
props: {
variant: 'home',
currentFloorId: 'floor-1',
currentFloorLabel: '1F'
}
})
await flushMountedSearch()
await wrapper.get('.search-box').trigger('tap')
await wrapper.get('.search-input').trigger('confirm', {
detail: { value: '洗手间' }
})
await flushMountedSearch()
expect(wrapper.find('.poi-search-content').exists()).toBe(true)
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(false)
wrapper.unmount()
})
it('详情返回专用复位恢复默认快捷入口并清空搜索状态', 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()
wrapper.get('[data-testid="poi-result-list"]').element.scrollTop = 126
await (wrapper.vm as unknown as {
returnToCollapsedAfterDetail: () => Promise<void>
}).returnToCollapsedAfterDetail()
await flushMountedSearch()
expect(wrapper.find('[data-testid="poi-home-category-results"]').exists()).toBe(false)
expect(wrapper.find('.poi-search-content').exists()).toBe(false)
expect(wrapper.findAll('.home-category-chip')).toHaveLength(5)
expect(wrapper.findAll('.home-category-chip.active')).toHaveLength(0)
expect((wrapper.get('.search-input').element as HTMLInputElement).value).toBe('')
expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({
active: false,
keyword: '',
categoryId: '',
visiblePoiIds: [],
listScrollTop: 0
})
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()
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('.search-box').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()
})
})