This commit is contained in:
40
tests/e2e/explain-hall-page.spec.ts
Normal file
40
tests/e2e/explain-hall-page.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
const stops = Array.from({ length: 21 }, (_, index) => ({
|
||||
stopId: `stop-${index + 1}`,
|
||||
name: `讲解对象${index + 1}`,
|
||||
imageStatus: index === 0 ? 'READY' : 'MISSING',
|
||||
coverImageUrl: index === 0 ? '/one.jpg' : undefined,
|
||||
playTargetType: 'STOP' as const,
|
||||
playTargetId: `target-${index + 1}`
|
||||
}))
|
||||
|
||||
test('hall goes directly to paged guide objects without outline requests', async ({ page }) => {
|
||||
const requests: string[] = []
|
||||
await page.route('**/app-api/gis/guide/catalog/**', async (route) => {
|
||||
const url = route.request().url()
|
||||
requests.push(url)
|
||||
if (url.includes('/catalog/halls?')) {
|
||||
await route.fulfill({ json: { code: 0, data: [{ id: 'hall-1', name: '恐龙厅', floorId: 'L1', stopCount: 21 }] } })
|
||||
return
|
||||
}
|
||||
if (url.includes('/halls/hall-1/stops/page')) {
|
||||
const pageNo = new URL(url).searchParams.get('pageNo')
|
||||
await route.fulfill({ json: { code: 0, data: { total: 21, list: pageNo === '1' ? stops.slice(0, 20) : stops.slice(20) } } })
|
||||
return
|
||||
}
|
||||
await route.fulfill({ status: 404, json: { code: 404, msg: 'unexpected request' } })
|
||||
})
|
||||
|
||||
await page.goto('/#/pages/explain/list')
|
||||
await page.getByText('恐龙厅', { exact: true }).click()
|
||||
await expect(page).toHaveURL(/guide-stop-list\?hallId=hall-1/)
|
||||
await expect(page.getByText('讲解对象1', { exact: true })).toBeVisible()
|
||||
await page.locator('.explain-scroll').hover()
|
||||
await page.mouse.wheel(0, 10000)
|
||||
await expect(page.getByText('讲解对象21', { exact: true })).toBeVisible()
|
||||
await page.getByText('讲解对象1', { exact: true }).click()
|
||||
await expect(page).toHaveURL(/pages\/exhibit\/detail\?.*targetType=STOP.*targetId=target-1/)
|
||||
expect(requests.some((url) => url.includes('/outlines'))).toBe(false)
|
||||
expect(requests.filter((url) => url.includes('/stops/page')).map((url) => new URL(url).searchParams.get('pageNo'))).toEqual(['1', '2'])
|
||||
})
|
||||
57
tests/unit/BackendExplainContentProvider.spec.ts
Normal file
57
tests/unit/BackendExplainContentProvider.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BackendExplainContentProvider } from '@/data/providers/backendExplainContentProvider'
|
||||
|
||||
const installRequest = (handler: (url: string) => unknown) => {
|
||||
vi.stubGlobal('uni', {
|
||||
request: ({ url, success }: { url: string, success: (response: unknown) => void }) => {
|
||||
success({ statusCode: 200, data: handler(url) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('BackendExplainContentProvider hall stop paging', () => {
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
it('uses the paged hall endpoint and maps independent pages without outlines', async () => {
|
||||
const requests: string[] = []
|
||||
installRequest((url) => {
|
||||
requests.push(url)
|
||||
if (url.includes('/catalog/halls?')) return { code: 0, data: [{ id: 'hall-1', name: '恐龙厅', floorId: 'f1' }] }
|
||||
const pageNo = new URL(url, 'http://localhost').searchParams.get('pageNo')
|
||||
return {
|
||||
code: 0,
|
||||
data: {
|
||||
total: 30,
|
||||
list: pageNo === '1'
|
||||
? [{ stopId: 'stop-1', name: '对象一', imageStatus: 'READY', coverImageUrl: '/one.jpg', playTargetType: 'STOP', playTargetId: 'target-1' }]
|
||||
: [{ stopId: 'stop-2', name: '对象二', imageStatus: 'MISSING', playTargetType: 'STOP', playTargetId: 'target-2' }]
|
||||
}
|
||||
}
|
||||
})
|
||||
const provider = new BackendExplainContentProvider()
|
||||
const [first, second] = await Promise.all([
|
||||
provider.listGuideStopsPageByHall('hall-1', 1, 20),
|
||||
provider.listGuideStopsPageByHall('hall-1', 2, 20)
|
||||
])
|
||||
|
||||
expect(first).toMatchObject({ total: 30, pageNo: 1, pageSize: 20, hasMore: true })
|
||||
expect(first.items[0]).toMatchObject({ id: 'stop-1', hallName: '恐龙厅', playTargetId: 'target-1' })
|
||||
expect(second.items.map((item) => item.id)).toEqual(['stop-2'])
|
||||
expect(requests.filter((url) => url.includes('/stops/page'))).toHaveLength(2)
|
||||
expect(requests.some((url) => url.includes('/outlines'))).toBe(false)
|
||||
expect(requests.find((url) => url.includes('pageNo=1') && url.includes('pageSize=20') && url.includes('lang=zh-CN'))).toBeTruthy()
|
||||
})
|
||||
|
||||
it('clears failed inflight requests so the page can be retried', async () => {
|
||||
let attempts = 0
|
||||
installRequest((url) => {
|
||||
if (url.includes('/catalog/halls?')) return { code: 0, data: [{ id: 'hall-1', name: '恐龙厅' }] }
|
||||
attempts += 1
|
||||
return attempts === 1 ? { code: 500, msg: 'temporary failure' } : { code: 0, data: { list: [], total: 0 } }
|
||||
})
|
||||
const provider = new BackendExplainContentProvider()
|
||||
await expect(provider.listGuideStopsPageByHall('hall-1', 1, 20)).rejects.toThrow('temporary failure')
|
||||
await expect(provider.listGuideStopsPageByHall('hall-1', 1, 20)).resolves.toMatchObject({ items: [], total: 0, hasMore: false })
|
||||
expect(attempts).toBe(2)
|
||||
})
|
||||
})
|
||||
122
tests/unit/ExplainGuideStopListNavigation.spec.ts
Normal file
122
tests/unit/ExplainGuideStopListNavigation.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ExplainGuideStopListPage from '@/pages/explain/guide-stop-list.vue'
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
onLoadHandler: null as null | ((options?: Record<string, string>) => void)
|
||||
}))
|
||||
|
||||
vi.mock('@dcloudio/uni-app', () => ({
|
||||
onLoad: (handler: (options?: Record<string, string>) => void) => {
|
||||
testState.onLoadHandler = handler
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/usecases/explainUseCase', () => ({
|
||||
explainUseCase: {
|
||||
listGuideStopsPageByHall: vi.fn().mockResolvedValue({
|
||||
items: [], total: 0, pageNo: 1, pageSize: 20, hasMore: false
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
const GuidePageFrameStub = defineComponent({
|
||||
name: 'GuidePageFrame',
|
||||
emits: ['back'],
|
||||
template: '<main><button data-testid="frame-back" @click="$emit(\'back\')">返回</button><slot /></main>'
|
||||
})
|
||||
|
||||
const ExplainHallSelectStub = defineComponent({
|
||||
name: 'ExplainHallSelect',
|
||||
emits: ['back'],
|
||||
template: '<button data-testid="list-back" @click="$emit(\'back\')">返回</button>'
|
||||
})
|
||||
|
||||
const mountGuideStopList = () => mount(ExplainGuideStopListPage, {
|
||||
global: {
|
||||
stubs: {
|
||||
GuidePageFrame: GuidePageFrameStub,
|
||||
ExplainHallSelect: ExplainHallSelectStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
testState.onLoadHandler = null
|
||||
vi.stubGlobal('uni', {
|
||||
navigateBack: vi.fn(),
|
||||
reLaunch: vi.fn(),
|
||||
navigateTo: vi.fn(),
|
||||
setNavigationBarTitle: vi.fn()
|
||||
})
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => []))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('讲解对象列表返回', () => {
|
||||
it('上一页是独立展厅列表时回到该列表', async () => {
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/explain/list', options: {} },
|
||||
{ route: 'pages/explain/guide-stop-list', options: {} }
|
||||
]))
|
||||
const wrapper = mountGuideStopList()
|
||||
testState.onLoadHandler?.({ hallId: 'hall-1', hallName: '恐龙厅' })
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('[data-testid="frame-back"]').trigger('click')
|
||||
|
||||
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
|
||||
expect(uni.reLaunch).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('上一页是首页讲解 Tab 时回到首页中的展厅列表', async () => {
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/index/index', options: { tab: 'explain' } },
|
||||
{ route: 'pages/explain/guide-stop-list', options: {} }
|
||||
]))
|
||||
const wrapper = mountGuideStopList()
|
||||
|
||||
await wrapper.get('[data-testid="list-back"]').trigger('click')
|
||||
|
||||
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
|
||||
expect(uni.reLaunch).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('深链或不相关上一页直接回退至独立展厅列表', async () => {
|
||||
const wrapper = mountGuideStopList()
|
||||
|
||||
await wrapper.get('[data-testid="list-back"]').trigger('click')
|
||||
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(uni.reLaunch).toHaveBeenCalledWith({ url: '/pages/explain/list' })
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('页面栈返回失败时回退至独立展厅列表', async () => {
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/explain/list', options: {} },
|
||||
{ route: 'pages/explain/guide-stop-list', options: {} }
|
||||
]))
|
||||
vi.stubGlobal('uni', {
|
||||
navigateBack: vi.fn((options: { fail?: () => void }) => options.fail?.()),
|
||||
reLaunch: vi.fn(),
|
||||
navigateTo: vi.fn(),
|
||||
setNavigationBarTitle: vi.fn()
|
||||
})
|
||||
const wrapper = mountGuideStopList()
|
||||
|
||||
await wrapper.get('[data-testid="list-back"]').trigger('click')
|
||||
|
||||
expect(uni.reLaunch).toHaveBeenCalledWith({ url: '/pages/explain/list' })
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -9,38 +9,30 @@ vi.mock('@/utils/hostEnvironment', () => ({
|
||||
}))
|
||||
|
||||
describe('讲解展厅选择列表', () => {
|
||||
it('业务单元卡片不显示业务单元标签,并保留讲解数量和点击事件', async () => {
|
||||
it('展厅卡片只显示讲解对象数量,不暴露业务单元契约', async () => {
|
||||
const wrapper = mount(ExplainHallSelect, {
|
||||
props: {
|
||||
stage: 'unit',
|
||||
businessUnits: [
|
||||
stage: 'hall',
|
||||
halls: [
|
||||
{
|
||||
id: 'unit-evolution',
|
||||
name: '生命演化',
|
||||
hallId: 'hall-evolution',
|
||||
guideStopCount: 6
|
||||
},
|
||||
{
|
||||
id: 'unit-dinosaur',
|
||||
name: '恐龙世界',
|
||||
hallId: 'hall-dinosaur',
|
||||
guideStopCount: 4
|
||||
id: 'hall-evolution',
|
||||
name: '生命演化厅',
|
||||
explainCount: 6,
|
||||
guideStopCount: 6,
|
||||
searchText: '生命演化厅'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const cards = wrapper.findAll('.unit-card')
|
||||
expect(cards).toHaveLength(2)
|
||||
expect(wrapper.text()).toContain('生命演化')
|
||||
expect(wrapper.text()).toContain('恐龙世界')
|
||||
expect(wrapper.text()).toContain('6 个讲解')
|
||||
expect(wrapper.text()).toContain('4 个讲解')
|
||||
expect(cards.map((card) => card.text()).join('')).not.toContain('业务单元')
|
||||
expect(cards[0]?.find('.floor-badge').exists()).toBe(false)
|
||||
const cards = wrapper.findAll('.hall-card')
|
||||
expect(cards).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('生命演化厅')
|
||||
expect(wrapper.text()).toContain('6 个讲解对象')
|
||||
expect(wrapper.text()).not.toContain('业务单元')
|
||||
|
||||
await cards[0]?.trigger('tap')
|
||||
expect(wrapper.emitted('businessUnitClick')).toEqual([['unit-evolution']])
|
||||
expect(wrapper.emitted('hallClick')).toEqual([['hall-evolution']])
|
||||
})
|
||||
|
||||
it('READY 讲解点不显示可讲解标签,非 READY 讲解点保留图文标签和点击事件', async () => {
|
||||
|
||||
13
tests/unit/ExplainNavigation.spec.ts
Normal file
13
tests/unit/ExplainNavigation.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { explainGuideStopListUrl } from '@/utils/explainNavigation'
|
||||
|
||||
describe('explain navigation', () => {
|
||||
it('builds the hall-only guide-object URL', () => {
|
||||
const url = explainGuideStopListUrl('hall-1', '恐龙厅')
|
||||
expect(url.split('?')[0]).toBe('/pages/explain/guide-stop-list')
|
||||
const params = new URLSearchParams(url.split('?')[1])
|
||||
expect(params.get('hallId')).toBe('hall-1')
|
||||
expect(params.get('hallName')).toBe('恐龙厅')
|
||||
expect(params.has('unitId')).toBe(false)
|
||||
})
|
||||
})
|
||||
102
tests/unit/ExplainUseCase.spec.ts
Normal file
102
tests/unit/ExplainUseCase.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { GuideStopInfo } from '@/data/adapters/guideStopInfoAdapter'
|
||||
import type { AudioPlayInfoRepository } from '@/repositories/AudioPlayInfoRepository'
|
||||
import type { ExplainRepository } from '@/repositories/ExplainRepository'
|
||||
import { ExplainUseCase } from '@/usecases/explainUseCase'
|
||||
import { dataSourceConfig } from '@/config/dataSource'
|
||||
|
||||
const createStopInfo = (overrides: Partial<GuideStopInfo> = {}): GuideStopInfo => ({
|
||||
available: true,
|
||||
targetType: 'STOP',
|
||||
targetId: 'stop-1',
|
||||
lang: 'zh-CN',
|
||||
title: 'Guide stop',
|
||||
galleryUrls: [],
|
||||
imageStatus: 'MISSING',
|
||||
linkedExhibits: [
|
||||
{
|
||||
id: 'exhibit-1',
|
||||
name: 'Linked exhibit',
|
||||
coverImageUrl: '/linked-exhibit.jpg'
|
||||
}
|
||||
],
|
||||
playTargetType: 'STOP',
|
||||
playTargetId: 'stop-1',
|
||||
hasAudio: false,
|
||||
hasText: false,
|
||||
supportedLanguages: [],
|
||||
audioStatus: 'MISSING',
|
||||
...overrides
|
||||
})
|
||||
|
||||
const createUseCase = (stopInfo: GuideStopInfo) => {
|
||||
const explain = {
|
||||
getExhibitById: vi.fn().mockResolvedValue(null),
|
||||
listExplainExhibits: vi.fn().mockResolvedValue([])
|
||||
} as unknown as ExplainRepository
|
||||
const audioPlayInfo = {
|
||||
getStopInfo: vi.fn().mockResolvedValue(stopInfo)
|
||||
} as unknown as AudioPlayInfoRepository
|
||||
|
||||
return new ExplainUseCase(explain, audioPlayInfo)
|
||||
}
|
||||
|
||||
describe('ExplainUseCase stop image policy', () => {
|
||||
it('uses stop-info first in remote mode without requesting a catalog fallback', async () => {
|
||||
const originalMode = dataSourceConfig.explainContentMode
|
||||
Object.assign(dataSourceConfig, { explainContentMode: 'remote' })
|
||||
const explain = {
|
||||
getExhibitById: vi.fn(),
|
||||
listExplainExhibits: vi.fn(),
|
||||
listExplainExhibitsByHall: vi.fn()
|
||||
} as unknown as ExplainRepository
|
||||
const audio = { getStopInfo: vi.fn().mockResolvedValue(createStopInfo()) } as unknown as AudioPlayInfoRepository
|
||||
|
||||
await new ExplainUseCase(explain, audio).enterExplainDetail({
|
||||
exhibitId: 'stop-1', targetType: 'STOP', targetId: 'stop-1', hallName: '宇宙厅'
|
||||
})
|
||||
|
||||
expect(audio.getStopInfo).toHaveBeenCalledTimes(1)
|
||||
expect(explain.getExhibitById).not.toHaveBeenCalled()
|
||||
expect(explain.listExplainExhibits).not.toHaveBeenCalled()
|
||||
Object.assign(dataSourceConfig, { explainContentMode: originalMode })
|
||||
})
|
||||
|
||||
it('delegates hall exhibits and business-unit stops without loading full exhibits', async () => {
|
||||
const explain = {
|
||||
listExplainExhibitsByHall: vi.fn().mockResolvedValue([]),
|
||||
listGuideStopsByBusinessUnit: vi.fn().mockResolvedValue([])
|
||||
} as unknown as ExplainRepository
|
||||
const useCase = new ExplainUseCase(explain, {} as AudioPlayInfoRepository)
|
||||
await useCase.listExhibitsByHallId('hall-1')
|
||||
await useCase.listGuideStopsByBusinessUnit('hall-1', 'outline-1')
|
||||
expect(explain.listExplainExhibitsByHall).toHaveBeenCalledWith('hall-1')
|
||||
expect(explain.listGuideStopsByBusinessUnit).toHaveBeenCalledWith('hall-1', 'outline-1')
|
||||
})
|
||||
it('does not use linked exhibit images when stop-info reports MISSING', async () => {
|
||||
const detail = await createUseCase(createStopInfo()).enterExplainDetail({
|
||||
exhibitId: 'stop-1',
|
||||
targetType: 'STOP',
|
||||
targetId: 'stop-1'
|
||||
})
|
||||
|
||||
expect(detail.image).toBeUndefined()
|
||||
expect(detail.galleryUrls).toEqual([])
|
||||
expect(detail.linkedExhibits?.[0]?.coverImageUrl).toBe('/linked-exhibit.jpg')
|
||||
})
|
||||
|
||||
it('uses the stop image when stop-info reports READY', async () => {
|
||||
const detail = await createUseCase(createStopInfo({
|
||||
imageStatus: 'READY',
|
||||
coverImageUrl: '/guide-stop.jpg',
|
||||
galleryUrls: ['/guide-stop-gallery.jpg']
|
||||
})).enterExplainDetail({
|
||||
exhibitId: 'stop-1',
|
||||
targetType: 'STOP',
|
||||
targetId: 'stop-1'
|
||||
})
|
||||
|
||||
expect(detail.image).toBe('/guide-stop.jpg')
|
||||
expect(detail.galleryUrls).toEqual(['/guide-stop-gallery.jpg'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user