index.test.js 12 KB
/**
 * 搜索页面测试套件
 * @description 测试搜索功能的各种场景,包括输入、返回、状态切换等
 * @date 2026-01-31
 */

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import SearchPage from './index.vue'

// Mock Taro
vi.mock('@tarojs/taro', () => ({
  default: {
    showToast: vi.fn()
  }
}))

// Mock hooks
vi.mock('@/hooks/useGo', () => ({
  useGo: () => vi.fn()
}))

describe('搜索页面测试', () => {
  let wrapper

  beforeEach(() => {
    wrapper = mount(SearchPage, {
      global: {
        components: {
          NavHeader: { template: '<div>NavHeader</div>' },
          IconFont: { template: '<div>IconFont</div>' },
          SearchBar: {
            template: '<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" @search="$emit(\'search\')" @clear="$emit(\'clear\')" @blur="$emit(\'blur\')" />',
            props: ['modelValue'],
            emits: ['update:modelValue', 'search', 'clear', 'blur']
          }
        }
      }
    })
  })

  describe('初始化状态测试', () => {
    it('应该正确初始化 mock 数据', () => {
      // 验证是否生成了 100 条数据(50 产品 + 50 资料)
      expect(wrapper.vm.allData.length).toBe(100)
    })

    it('应该正确初始化分类数据', () => {
      // 验证 3 个分类(全部、产品、资料)
      expect(wrapper.vm.tabsData.length).toBe(3)
      expect(wrapper.vm.tabsData[0].name).toBe('全部')
      expect(wrapper.vm.tabsData[1].name).toBe('产品')
      expect(wrapper.vm.tabsData[2].name).toBe('资料')
    })

    it('应该默认选中"全部"分类', () => {
      expect(wrapper.vm.activeTabId).toBe('')
    })

    it('初始状态 shouldSearched 应该为 false', () => {
      expect(wrapper.vm.hasSearched).toBe(false)
    })

    it('初始状态搜索结果应该为空数组', () => {
      expect(wrapper.vm.searchResults.length).toBe(0)
    })
  })

  describe('搜索功能测试', () => {
    it('输入搜索关键词后,hasSearched 应该变为 true', async () => {
      wrapper.vm.searchKeyword = '保险'
      await wrapper.vm.$nextTick()
      await wrapper.vm.handleSearch()

      expect(wrapper.vm.hasSearched).toBe(true)
      expect(wrapper.vm.searchKeyword).toBe('保险')
    })

    it('应该能搜索到包含关键词的产品', async () => {
      wrapper.vm.searchKeyword = '保险'
      await wrapper.vm.$nextTick()
      await wrapper.vm.handleSearch()

      const results = wrapper.vm.searchResults
      expect(results.length).toBeGreaterThan(0)
      expect(results[0].title.toLowerCase()).toContain('保险')
    })

    it('应该能搜索到包含关键词的资料', async () => {
      wrapper.vm.searchKeyword = '培训'
      await wrapper.vm.$nextTick()
      await wrapper.vm.handleSearch()

      const results = wrapper.vm.searchResults
      expect(results.length).toBeGreaterThan(0)
      expect(results[0].title.toLowerCase()).toContain('培训')
    })

    it('搜索不存在的关键词应该返回空结果', async () => {
      wrapper.vm.searchKeyword = '不存在的内容xyz123'
      await wrapper.vm.$nextTick()
      await wrapper.vm.handleSearch()

      expect(wrapper.vm.hasSearched).toBe(true)
      expect(wrapper.vm.searchResults.length).toBe(0)
    })

    it('清空搜索关键词后 hasSearched 应该保持为 true', async () => {
      // 先执行搜索
      wrapper.vm.searchKeyword = '保险'
      await wrapper.vm.$nextTick()
      await wrapper.vm.handleSearch()
      expect(wrapper.vm.hasSearched).toBe(true)

      // 清空搜索
      await wrapper.vm.clearSearch()
      expect(wrapper.vm.searchKeyword).toBe('')
      // ✅ 修复后:hasSearched 应该保持 true
      expect(wrapper.vm.hasSearched).toBe(true)
    })
  })

  describe('分类切换测试', () => {
    beforeEach(async () => {
      // 先执行一次搜索
      wrapper.vm.searchKeyword = '保险'
      await wrapper.vm.handleSearch()
    })

    it('切换到"产品"分类应该只显示产品', async () => {
      await wrapper.vm.onTabClick('product')

      expect(wrapper.vm.activeTabId).toBe('product')
      const results = wrapper.vm.searchResults

      results.forEach(item => {
        expect(item.category).toBe('product')
      })
    })

    it('切换到"资料"分类应该只显示资料', async () => {
      await wrapper.vm.onTabClick('material')

      expect(wrapper.vm.activeTabId).toBe('material')
      const results = wrapper.vm.searchResults

      results.forEach(item => {
        expect(item.category).toBe('material')
      })
    })

    it('切换到"全部"分类应该显示所有结果', async () => {
      await wrapper.vm.onTabClick('')

      expect(wrapper.vm.activeTabId).toBe('')
      const results = wrapper.vm.searchResults

      // 验证包含所有分类
      const hasProduct = results.some(item => item.category === 'product')
      const hasMaterial = results.some(item => item.category === 'material')

      expect(hasProduct).toBe(true)
      expect(hasMaterial).toBe(true)
    })

    it('切换分类后 listRenderKey 应该递增', async () => {
      const oldKey = wrapper.vm.listRenderKey
      await wrapper.vm.onTabClick('product')

      expect(wrapper.vm.listRenderKey).toBe(oldKey + 1)
    })
  })

  describe('实时搜索测试(watch searchKeyword)', () => {
    it('输入关键词应该自动触发搜索', async () => {
      wrapper.vm.searchKeyword = '医疗'

      // 等待 watch 触发
      await wrapper.vm.$nextTick()

      expect(wrapper.vm.hasSearched).toBe(true)
      expect(wrapper.vm.searchResults.length).toBeGreaterThan(0)
    })

    it('清空关键词应该自动重置搜索状态', async () => {
      // 先输入
      wrapper.vm.searchKeyword = '保险'
      await wrapper.vm.$nextTick()
      expect(wrapper.vm.hasSearched).toBe(true)

      // 清空
      wrapper.vm.searchKeyword = ''
      await wrapper.vm.$nextTick()

      expect(wrapper.vm.hasSearched).toBe(false)
      expect(wrapper.vm.searchResults.length).toBe(0)
    })
  })

  describe('边界情况测试', () => {
    it('输入纯空格不应该触发搜索', async () => {
      wrapper.vm.searchKeyword = '   '
      await wrapper.vm.handleSearch()

      expect(wrapper.vm.hasSearched).toBe(false)
    })

    it('输入特殊字符应该正常搜索', async () => {
      wrapper.vm.searchKeyword = '!@#$%'
      await wrapper.vm.handleSearch()

      expect(wrapper.vm.hasSearched).toBe(true)
      // 应该返回空结果,但不应该报错
      expect(wrapper.vm.searchResults.length).toBe(0)
    })

    it('输入超长关键词应该正常处理', async () => {
      const longKeyword = 'a'.repeat(1000)
      wrapper.vm.searchKeyword = longKeyword
      await wrapper.vm.handleSearch()

      expect(wrapper.vm.hasSearched).toBe(true)
      // 应该返回空结果,但不应该报错
      expect(wrapper.vm.searchResults.length).toBe(0)
    })

    it('快速切换多次分类不应该出错', async () => {
      wrapper.vm.searchKeyword = '保险'
      await wrapper.vm.handleSearch()

      // 快速切换分类
      await wrapper.vm.onTabClick('product')
      await wrapper.vm.onTabClick('material')
      await wrapper.vm.onTabClick('')
      await wrapper.vm.onTabClick('product')

      // 不应该抛出错误
      expect(wrapper.vm.activeTabId).toBe('product')
    })

    it('快速输入和删除关键词应该正常处理', async () => {
      // 快速输入
      wrapper.vm.searchKeyword = '保'
      await wrapper.vm.$nextTick()

      wrapper.vm.searchKeyword = '保险'
      await wrapper.vm.$nextTick()

      wrapper.vm.searchKeyword = '保险产品'
      await wrapper.vm.$nextTick()

      // 快速删除
      wrapper.vm.searchKeyword = '保险'
      await wrapper.vm.$nextTick()

      wrapper.vm.searchKeyword = ''
      await wrapper.vm.$nextTick()

      // 最终状态应该是重置的
      expect(wrapper.vm.hasSearched).toBe(false)
      expect(wrapper.vm.searchResults.length).toBe(0)
    })
  })

  describe('✅ 修复后的问题测试', () => {
    it('问题1修复: 清空关键词后,应该显示"暂无搜索结果"而不是"初始状态"', async () => {
      // 1. 先搜索一个关键词(有结果)
      wrapper.vm.searchKeyword = '保险'
      await wrapper.vm.handleSearch()

      console.log('搜索"保险"后:')
      console.log('- hasSearched:', wrapper.vm.hasSearched)
      console.log('- results.length:', wrapper.vm.searchResults.length)

      expect(wrapper.vm.hasSearched).toBe(true)
      expect(wrapper.vm.searchResults.length).toBeGreaterThan(0)

      // 2. 清空关键词
      wrapper.vm.searchKeyword = ''
      await wrapper.vm.$nextTick()

      console.log('清空关键词后:')
      console.log('- hasSearched:', wrapper.vm.hasSearched)
      console.log('- results.length:', wrapper.vm.searchResults.length)
      console.log('- isInitialState:', wrapper.vm.isInitialState)

      // ✅ 修复后:hasSearched 保持 true
      expect(wrapper.vm.hasSearched).toBe(true)
      expect(wrapper.vm.isInitialState).toBe(false)
      expect(wrapper.vm.searchResults.length).toBe(0)
      console.log('✅ FIXED: 用户看到"暂无搜索结果"而不是"初始状态"')
    })

    it('问题2: 切换分类时,如果当前没有关键词,不应该显示结果', async () => {
      // 不输入任何关键词,直接切换分类
      await wrapper.vm.onTabClick('product')

      // hasSearched 应该保持 false
      expect(wrapper.vm.hasSearched).toBe(false)
      // 结果应该为空
      expect(wrapper.vm.searchResults.length).toBe(0)
    })

    it('问题3: 搜索关键词后切换分类,结果应该正确更新', async () => {
      // 1. 搜索"保险"
      wrapper.vm.searchKeyword = '保险'
      await wrapper.vm.handleSearch()

      const allResults = wrapper.vm.searchResults.length
      console.log('全部分类搜索"保险"结果数:', allResults)

      // 2. 切换到"产品"分类
      await wrapper.vm.onTabClick('product')
      const productResults = wrapper.vm.searchResults.length
      console.log('产品分类搜索"保险"结果数:', productResults)

      // 3. 切换到"资料"分类
      await wrapper.vm.onTabClick('material')
      const materialResults = wrapper.vm.searchResults.length
      console.log('资料分类搜索"保险"结果数:', materialResults)

      // 验证结果正确性
      expect(productResults + materialResults).toBe(allResults)
    })
  })
})

describe('搜索结果 computed 属性详细测试', () => {
  let wrapper

  beforeEach(() => {
    wrapper = mount(SearchPage, {
      global: {
        components: {
          NavHeader: { template: '<div>NavHeader</div>' },
          IconFont: { template: '<div>IconFont</div>' },
          SearchBar: { template: '<div>SearchBar</div>' }
        }
      }
    })
  })

  it('当 hasSearched 为 false 时,应该返回空数组', () => {
    wrapper.vm.hasSearched = false
    wrapper.vm.searchKeyword = '保险'

    expect(wrapper.vm.searchResults.length).toBe(0)
  })

  it('当 hasSearched 为 true 但关键词为空时,应该返回当前分类的所有数据', () => {
    wrapper.vm.hasSearched = true
    wrapper.vm.searchKeyword = ''
    wrapper.vm.activeTabId = ''

    // 应该返回所有 100 条数据
    expect(wrapper.vm.searchResults.length).toBe(100)
  })

  it('当 hasSearched 为 true 且有关键词时,应该返回过滤后的结果', () => {
    wrapper.vm.hasSearched = true
    wrapper.vm.searchKeyword = '保险'
    wrapper.vm.activeTabId = ''

    const results = wrapper.vm.searchResults
    expect(results.length).toBeGreaterThan(0)

    // 验证所有结果都包含关键词
    results.forEach(item => {
      expect(item.title.toLowerCase()).toContain('保险')
    })
  })

  it('搜索应该不区分大小写', () => {
    wrapper.vm.hasSearched = true
    wrapper.vm.searchKeyword = 'BAOXIAN'
    wrapper.vm.activeTabId = ''

    const results = wrapper.vm.searchResults
    // 应该能搜索到包含"保险"的内容
    expect(results.length).toBeGreaterThan(0)
  })
})