hensei-web/src/lib/api/adapters/__tests__/entity.adapter.test.ts
Justin Edmund c821873ac6 fix: test fixtures and awakening type issues
- remove recruits property from Character test mock (doesn't exist in type)
- add missing subaura property to Summon test mocks
- consolidate Awakening imports to use entities source
- make awakening type/level optional in GridWeapon/GridSummon
- fix null handling in AwakeningDisplay (null -> undefined)
2025-11-28 21:04:26 -08:00

395 lines
No EOL
9 KiB
TypeScript

/**
* Tests for EntityAdapter
*
* These tests verify read-only access to canonical game data
* for weapons, characters, and summons.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { EntityAdapter } from '../entity.adapter'
import type { Weapon, Character, Summon } from '../entity.adapter'
describe('EntityAdapter', () => {
let adapter: EntityAdapter
let originalFetch: typeof global.fetch
const mockWeapon: Weapon = {
id: 'weapon-1',
granblueId: '1040001',
name: {
en: 'Sword of Justice',
ja: '正義の剣'
},
rarity: 4,
element: 1,
proficiency: 1,
series: 1,
weaponType: 1,
minHp: 100,
maxHp: 500,
minAttack: 200,
maxAttack: 1000,
flbHp: 600,
flbAttack: 1200,
ulbHp: 700,
ulbAttack: 1400,
transcendenceHp: 800,
transcendenceAttack: 1600,
awakenings: [
{
id: 'awk-1',
name: { en: 'Attack Boost' },
level: 1
}
]
}
const mockCharacter: Character = {
id: 'char-1',
granblueId: '3040001',
name: {
en: 'Hero',
ja: '英雄'
},
rarity: 5,
element: 2,
uncap: {
flb: true,
ulb: true
},
special: false,
gender: 0,
race: [1, 2],
proficiency: [1, 2],
hp: {
minHp: 150,
maxHp: 750,
maxHpFlb: 900
},
atk: {
minAtk: 250,
maxAtk: 1250,
maxAtkFlb: 1500
}
}
const mockSummon: Summon = {
id: 'summon-1',
granblueId: '2040001',
name: {
en: 'Bahamut',
ja: 'バハムート'
},
rarity: 5,
element: 0,
series: 2,
minHp: 200,
maxHp: 1000,
minAttack: 300,
maxAttack: 1500,
flbHp: 1200,
flbAttack: 1800,
ulbHp: 1400,
ulbAttack: 2100,
transcendenceHp: 1600,
transcendenceAttack: 2400,
subaura: false,
cooldown: 9
}
beforeEach(() => {
originalFetch = global.fetch
adapter = new EntityAdapter({ baseURL: 'https://api.example.com' })
})
afterEach(() => {
global.fetch = originalFetch
vi.clearAllTimers()
})
describe('weapon operations', () => {
it('should get a weapon by ID', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockWeapon
})
const result = await adapter.getWeapon('weapon-1')
expect(result).toEqual(mockWeapon)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/weapons/weapon-1',
expect.objectContaining({
method: 'GET'
})
)
})
it('should batch fetch multiple weapons', async () => {
const mockWeapon2 = { ...mockWeapon, id: 'weapon-2' }
global.fetch = vi
.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => mockWeapon
})
.mockResolvedValueOnce({
ok: true,
json: async () => mockWeapon2
})
const result = await adapter.getWeapons(['weapon-1', 'weapon-2'])
expect(result).toEqual([mockWeapon, mockWeapon2])
expect(global.fetch).toHaveBeenCalledTimes(2)
})
})
describe('character operations', () => {
it('should get a character by ID', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockCharacter
})
const result = await adapter.getCharacter('char-1')
expect(result).toEqual(mockCharacter)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/characters/char-1',
expect.objectContaining({
method: 'GET'
})
)
})
it('should batch fetch multiple characters', async () => {
const mockCharacter2 = { ...mockCharacter, id: 'char-2' }
global.fetch = vi
.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => mockCharacter
})
.mockResolvedValueOnce({
ok: true,
json: async () => mockCharacter2
})
const result = await adapter.getCharacters(['char-1', 'char-2'])
expect(result).toEqual([mockCharacter, mockCharacter2])
expect(global.fetch).toHaveBeenCalledTimes(2)
})
})
describe('summon operations', () => {
it('should get a summon by ID', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockSummon
})
const result = await adapter.getSummon('summon-1')
expect(result).toEqual(mockSummon)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/summons/summon-1',
expect.objectContaining({
method: 'GET'
})
)
})
it('should batch fetch multiple summons', async () => {
const mockSummon2 = { ...mockSummon, id: 'summon-2' }
global.fetch = vi
.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => mockSummon
})
.mockResolvedValueOnce({
ok: true,
json: async () => mockSummon2
})
const result = await adapter.getSummons(['summon-1', 'summon-2'])
expect(result).toEqual([mockSummon, mockSummon2])
expect(global.fetch).toHaveBeenCalledTimes(2)
})
})
describe('caching', () => {
it('should cache entity requests', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockWeapon
})
// First call
await adapter.getWeapon('weapon-1')
// Second call (should use cache)
await adapter.getWeapon('weapon-1')
// Should only call fetch once due to caching
expect(global.fetch).toHaveBeenCalledTimes(1)
})
it('should clear weapon cache', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockWeapon
})
// First call
await adapter.getWeapon('weapon-1')
// Clear weapon cache
adapter.clearEntityCache('weapons')
// Second call (should not use cache)
await adapter.getWeapon('weapon-1')
// Should call fetch twice since cache was cleared
expect(global.fetch).toHaveBeenCalledTimes(2)
})
it('should clear character cache', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockCharacter
})
// First call
await adapter.getCharacter('char-1')
// Clear character cache
adapter.clearEntityCache('characters')
// Second call (should not use cache)
await adapter.getCharacter('char-1')
// Should call fetch twice since cache was cleared
expect(global.fetch).toHaveBeenCalledTimes(2)
})
it('should clear summon cache', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockSummon
})
// First call
await adapter.getSummon('summon-1')
// Clear summon cache
adapter.clearEntityCache('summons')
// Second call (should not use cache)
await adapter.getSummon('summon-1')
// Should call fetch twice since cache was cleared
expect(global.fetch).toHaveBeenCalledTimes(2)
})
it('should clear all entity caches', async () => {
global.fetch = vi
.fn()
.mockImplementation((url) => {
if (url.includes('/weapons/')) {
return Promise.resolve({
ok: true,
json: async () => mockWeapon
})
} else if (url.includes('/characters/')) {
return Promise.resolve({
ok: true,
json: async () => mockCharacter
})
} else if (url.includes('/summons/')) {
return Promise.resolve({
ok: true,
json: async () => mockSummon
})
}
return Promise.reject(new Error('Unknown URL'))
})
// First calls
await adapter.getWeapon('weapon-1')
await adapter.getCharacter('char-1')
await adapter.getSummon('summon-1')
// Should have called fetch 3 times
expect(global.fetch).toHaveBeenCalledTimes(3)
// Clear all entity caches
adapter.clearEntityCache()
// Second calls (should not use cache)
await adapter.getWeapon('weapon-1')
await adapter.getCharacter('char-1')
await adapter.getSummon('summon-1')
// Should have called fetch 6 times total
expect(global.fetch).toHaveBeenCalledTimes(6)
})
})
describe('error handling', () => {
it('should handle 404 errors', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
json: async () => ({ error: 'Weapon not found' })
})
await expect(adapter.getWeapon('invalid-id')).rejects.toThrow()
})
it('should handle network errors', async () => {
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
await expect(adapter.getCharacter('char-1')).rejects.toThrow('Network error')
})
it('should handle JSON parse errors', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => {
throw new Error('Invalid JSON')
}
})
await expect(adapter.getSummon('summon-1')).rejects.toThrow()
})
})
describe('configuration', () => {
it('should use custom cache time', async () => {
const customAdapter = new EntityAdapter({
baseURL: 'https://api.example.com',
cacheTime: 60000 // 1 minute
})
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockWeapon
})
await customAdapter.getWeapon('weapon-1')
// The cache time is set internally, but we can verify it's configured
expect(customAdapter).toBeDefined()
})
it('should use default baseURL if not provided', () => {
const defaultAdapter = new EntityAdapter()
expect(defaultAdapter).toBeDefined()
})
})
})