hensei-web/src/lib/api/adapters/__tests__/search.adapter.test.ts

419 lines
No EOL
9 KiB
TypeScript

/**
* Tests for SearchAdapter
*
* These tests verify search functionality including filtering,
* pagination, and proper request/response handling.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { SearchAdapter } from '../search.adapter'
import type { SearchParams, SearchResponse } from '../search.adapter'
describe('SearchAdapter', () => {
let adapter: SearchAdapter
let originalFetch: typeof global.fetch
beforeEach(() => {
originalFetch = global.fetch
adapter = new SearchAdapter({
baseURL: 'https://api.example.com'
})
})
afterEach(() => {
global.fetch = originalFetch
adapter.cancelAll()
})
describe('searchAll', () => {
it('should search across all entity types', async () => {
const mockResponse: SearchResponse = {
results: [
{
id: '1',
granblueId: 'weapon_1',
name: { en: 'Bahamut Sword', ja: 'バハムートソード' },
element: 1,
rarity: 5,
searchableType: 'Weapon',
imageUrl: 'https://example.com/weapon1.jpg'
},
{
id: '2',
granblueId: 'character_1',
name: { en: 'Bahamut', ja: 'バハムート' },
element: 6,
rarity: 5,
searchableType: 'Character',
imageUrl: 'https://example.com/character1.jpg'
}
],
total: 2,
page: 1,
totalPages: 1,
meta: {
count: 2,
page: 1,
perPage: 20,
totalPages: 1
}
}
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
results: [
{
id: '1',
granblue_id: 'weapon_1',
name: { en: 'Bahamut Sword', ja: 'バハムートソード' },
element: 1,
rarity: 5,
searchable_type: 'Weapon',
image_url: 'https://example.com/weapon1.jpg'
},
{
id: '2',
granblue_id: 'character_1',
name: { en: 'Bahamut', ja: 'バハムート' },
element: 6,
rarity: 5,
searchable_type: 'Character',
image_url: 'https://example.com/character1.jpg'
}
],
total: 2,
page: 1,
total_pages: 1,
meta: {
count: 2,
page: 1,
per_page: 20,
total_pages: 1
}
})
})
const params: SearchParams = {
query: 'bahamut',
locale: 'en',
page: 1
}
const result = await adapter.searchAll(params)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/search/all',
expect.objectContaining({
method: 'POST',
credentials: 'omit',
body: JSON.stringify({
locale: 'en',
page: 1,
query: 'bahamut'
})
})
)
expect(result).toEqual(mockResponse)
})
it('should include filters when provided', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ results: [] })
})
const params: SearchParams = {
query: 'sword',
filters: {
element: [1, 2],
rarity: [5],
extra: true
}
}
await adapter.searchAll(params)
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: JSON.stringify({
locale: 'en',
page: 1,
query: 'sword',
filters: {
element: [1, 2],
rarity: [5],
extra: true
}
})
})
)
})
it('should handle exclude parameter', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ results: [] })
})
const params: SearchParams = {
query: 'test',
exclude: ['1', '2', '3']
}
await adapter.searchAll(params)
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: JSON.stringify({
locale: 'en',
page: 1,
query: 'test',
exclude: ['1', '2', '3']
})
})
)
})
it('should cache results when query is provided', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ results: [{ id: '1' }] })
})
// First call
await adapter.searchAll({ query: 'test' })
expect(global.fetch).toHaveBeenCalledTimes(1)
// Second call should use cache
await adapter.searchAll({ query: 'test' })
expect(global.fetch).toHaveBeenCalledTimes(1)
})
it('should not cache results when query is empty', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ results: [] })
})
// First call
await adapter.searchAll({})
expect(global.fetch).toHaveBeenCalledTimes(1)
// Second call should not use cache
await adapter.searchAll({})
expect(global.fetch).toHaveBeenCalledTimes(2)
})
})
describe('searchWeapons', () => {
it('should search for weapons with appropriate filters', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ results: [] })
})
const params: SearchParams = {
query: 'sword',
filters: {
element: [1],
proficiency1: [2],
extra: false
},
per: 50
}
await adapter.searchWeapons(params)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/search/weapons',
expect.objectContaining({
method: 'POST',
credentials: 'omit',
body: JSON.stringify({
locale: 'en',
page: 1,
per: 50,
query: 'sword',
filters: {
element: [1],
proficiency1: [2],
extra: false
}
})
})
)
})
it('should not include character-specific filters', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ results: [] })
})
const params: SearchParams = {
query: 'test',
filters: {
proficiency2: [1], // Character-only filter
subaura: true // Summon-only filter
}
}
await adapter.searchWeapons(params)
const calledBody = JSON.parse((global.fetch as any).mock.calls[0][1].body)
expect(calledBody.filters).toBeUndefined()
})
})
describe('searchCharacters', () => {
it('should search for characters with appropriate filters', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ results: [] })
})
const params: SearchParams = {
query: 'katalina',
filters: {
element: [2],
proficiency1: [1],
proficiency2: [3]
}
}
await adapter.searchCharacters(params)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/search/characters',
expect.objectContaining({
body: JSON.stringify({
locale: 'en',
page: 1,
query: 'katalina',
filters: {
element: [2],
proficiency1: [1],
proficiency2: [3]
}
})
})
)
})
})
describe('searchSummons', () => {
it('should search for summons with appropriate filters', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ results: [] })
})
const params: SearchParams = {
query: 'bahamut',
filters: {
element: [6],
rarity: [5],
subaura: true
}
}
await adapter.searchSummons(params)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/search/summons',
expect.objectContaining({
body: JSON.stringify({
locale: 'en',
page: 1,
query: 'bahamut',
filters: {
element: [6],
rarity: [5],
subaura: true
}
})
})
)
})
})
describe('clearSearchCache', () => {
it('should clear cached search results', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ results: [{ id: '1' }] })
})
// Cache a search
await adapter.searchAll({ query: 'test' })
expect(global.fetch).toHaveBeenCalledTimes(1)
// Clear cache
adapter.clearSearchCache()
// Should fetch again
await adapter.searchAll({ query: 'test' })
expect(global.fetch).toHaveBeenCalledTimes(2)
})
})
describe('error handling', () => {
it('should handle server errors', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: async () => ({ error: 'Server error' })
})
await expect(adapter.searchAll({ query: 'test' })).rejects.toMatchObject({
code: 'SERVER_ERROR',
status: 500
})
})
it('should handle network errors', async () => {
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
await expect(adapter.searchAll({ query: 'test' })).rejects.toMatchObject({
code: 'UNKNOWN_ERROR'
})
})
})
describe('pagination', () => {
it('should handle pagination parameters', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
results: []
})
})
const result = await adapter.searchAll({
query: 'test',
page: 2,
per: 20
})
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: JSON.stringify({
locale: 'en',
page: 2,
per: 20,
query: 'test'
})
})
)
// Unified search only returns results, no pagination metadata
expect(result.results).toEqual([])
})
})
})