// @vitest-environment happy-dom import { mount } from '@vue/test-utils' import { afterEach, describe, expect, it, vi } from 'vitest' import ExplainHallSelect from '@/components/explain/ExplainHallSelect.vue' const hostEnvironmentMocks = vi.hoisted(() => ({ embeddedInWechatMiniProgram: false })) vi.mock('@/utils/hostEnvironment', () => ({ isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram })) afterEach(() => { hostEnvironmentMocks.embeddedInWechatMiniProgram = false }) describe('讲解展厅选择列表', () => { it('微信嵌入态独立页面可强制显示 H5 返回入口,且不使用宿主导航布局', async () => { hostEnvironmentMocks.embeddedInWechatMiniProgram = true const wrapper = mount(ExplainHallSelect, { props: { forceInternalHeader: true } }) expect(wrapper.findAll('.explain-page-header')).toHaveLength(1) expect(wrapper.findAll('.header-back')).toHaveLength(1) expect(wrapper.classes()).not.toContain('host-navigation') await wrapper.get('.header-back').trigger('tap') expect(wrapper.emitted('back')).toEqual([[]]) }) it('微信嵌入态未强制时保留宿主导航布局,保护首页讲解 Tab', () => { hostEnvironmentMocks.embeddedInWechatMiniProgram = true const wrapper = mount(ExplainHallSelect) expect(wrapper.find('.explain-page-header').exists()).toBe(false) expect(wrapper.classes()).toContain('host-navigation') }) it('展厅卡片只显示讲解对象数量,不暴露业务单元契约', async () => { const wrapper = mount(ExplainHallSelect, { props: { stage: 'hall', halls: [ { id: 'hall-evolution', name: '生命演化厅', explainCount: 6, guideStopCount: 6, searchText: '生命演化厅' } ] } }) const cards = wrapper.findAll('.hall-card') expect(cards).toHaveLength(1) expect(wrapper.text()).toContain('生命演化厅') expect(wrapper.text()).not.toContain('6 个讲解对象') expect(wrapper.text()).not.toContain('业务单元') await cards[0]?.trigger('tap') expect(wrapper.emitted('hallClick')).toEqual([['hall-evolution']]) }) it('展厅阶段可输入讲解编号并保留前导零', async () => { const wrapper = mount(ExplainHallSelect, { props: { stage: 'hall' } }) expect(wrapper.get('.code-entry-title').text()).toBe('编号讲解') expect(wrapper.get('.hall-section-title').text()).toBe('按展厅选择') expect(wrapper.find('.code-entry-arrow').exists()).toBe(false) await wrapper.get('.code-entry-button').trigger('tap') expect(wrapper.get('.header-title').text()).toBe('编号讲解') expect(wrapper.find('.code-entry-close').exists()).toBe(false) expect(wrapper.get('.code-entry-sheet-title').text()).toBe('输入讲解编号') expect(wrapper.findAll('.code-digit-cell')).toHaveLength(4) expect(wrapper.text()).toContain('请输入展品标签上的四位编号') await wrapper.get('.code-entry-native-input').trigger('input', { detail: { value: '02a' } }) expect(wrapper.get('.code-entry-action.primary').attributes('disabled')).toBeDefined() await wrapper.get('.code-entry-native-input').trigger('confirm') expect(wrapper.get('.code-entry-error').text()).toBe('请输入完整的 4 位讲解编号') await wrapper.get('.code-entry-native-input').trigger('input', { detail: { value: '0231' } }) expect(wrapper.findAll('.code-digit-value').map((item) => item.text())).toEqual(['0', '2', '3', '1']) await wrapper.get('.code-entry-action.primary').trigger('tap') expect(wrapper.emitted('codeSubmit')).toEqual([['0231']]) expect(wrapper.find('.code-entry-sheet').exists()).toBe(true) wrapper.vm.completeCodeEntry() await wrapper.vm.$nextTick() expect(wrapper.find('.code-entry-sheet').exists()).toBe(true) expect(wrapper.findAll('.code-digit-value').map((item) => item.text())).toEqual(['', '', '', '']) await wrapper.setProps({ codeSubmitting: true }) expect(wrapper.get('.code-entry-native-input').attributes('disabled')).toBeDefined() expect(wrapper.get('.code-entry-action.primary').text()).toBe('正在查询') wrapper.vm.showCodeEntryError('未找到该讲解编号,请确认后重试') await wrapper.vm.$nextTick() expect(wrapper.get('.code-entry-error').text()).toBe('未找到该讲解编号,请确认后重试') await wrapper.get('.header-back').trigger('tap') expect(wrapper.find('.code-entry-sheet').exists()).toBe(false) expect(wrapper.get('.header-title').text()).toBe('免费讲解') expect(wrapper.emitted('back')).toBeUndefined() await wrapper.get('.header-back').trigger('tap') expect(wrapper.emitted('back')).toEqual([[]]) }) it('展厅阶段使用完整展厅卡片资源', () => { const wrapper = mount(ExplainHallSelect, { props: { stage: 'hall', halls: [{ id: 'hall-dinosaur', name: '恐龙厅', floorLabel: 'L-2', explainCount: 35, guideStopCount: 35, searchText: '恐龙厅' }] } }) const card = wrapper.get('.hall-overview-card') expect(wrapper.get('.header-title').text()).toBe('免费讲解') expect(card.attributes('style')).toContain('--hall-card-color: #bb965a') expect(card.get('.hall-full-card-image').attributes('src')) .toBe('/static/icons/halls/portal-icons/dinosaur.png') expect(card.find('.hall-overview-copy').exists()).toBe(false) expect(card.find('.hall-go-entry').exists()).toBe(false) }) it('讲解单元阶段显示单元目录并返回被选择的单元 ID', async () => { const wrapper = mount(ExplainHallSelect, { props: { stage: 'unit', selectedHallName: '生命演化厅', businessUnits: [{ id: 'unit-origin', name: '生命起源', hallId: 'hall-evolution', guideStopCount: 6 }] } }) const card = wrapper.get('.business-unit-card') expect(wrapper.get('.header-title').text()).toBe('生命演化厅') expect(card.text()).toContain('生命起源') expect(card.text()).toContain('6 个讲解点') await card.trigger('tap') expect(wrapper.emitted('businessUnitClick')).toEqual([['unit-origin']]) }) it('READY 讲解点不显示可讲解标签,非 READY 讲解点保留图文标签和点击事件', async () => { const readyStop = { id: 'stop-ready', name: '生命起源', hallId: 'hall-evolution', audioStatus: 'READY' } const textStop = { id: 'stop-text', name: '化石记录', hallId: 'hall-evolution', audioStatus: 'MISSING' } const wrapper = mount(ExplainHallSelect, { props: { stage: 'stop', guideStops: [readyStop, textStop] } }) const cards = wrapper.findAll('.stop-card') expect(cards).toHaveLength(2) expect(wrapper.text()).toContain('生命起源') expect(wrapper.text()).toContain('化石记录') expect(wrapper.text()).not.toContain('可讲解') expect(cards[0]?.find('.hall-meta-row').exists()).toBe(false) expect(cards[1]?.find('.floor-badge').text()).toBe('图文') await cards[0]?.trigger('tap') await cards[1]?.trigger('tap') expect(wrapper.emitted('guideStopClick')).toEqual([[readyStop], [textStop]]) }) })