hensei-web/src/lib/api/adapters/__tests__/grid.adapter.test.ts
Justin Edmund 1b00889e81
weapon series detail/edit pages (#449)
## Summary
- add detail and edit pages for weapon series at
`/database/series/weapons/[slug]`
- fix series table navigation to use slug instead of uuid
- add `?view=series` URL param for bookmarkable series view
- remove redundant `/database/series` page
- rename augment type `none` → `no_augment` for consistency

## Test plan
- [ ] navigate to /database/weapons, toggle to series view
- [ ] verify URL updates to ?view=series
- [ ] click a series row, should go to detail page
- [ ] back button returns to series view
- [ ] edit page saves correctly
2025-12-31 23:55:03 -08:00

532 lines
No EOL
12 KiB
TypeScript

/**
* Tests for GridAdapter
*
* These tests verify grid item CRUD operations, position management,
* uncap updates, and conflict resolution functionality.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { GridAdapter } from '../grid.adapter'
import type { GridWeapon, GridCharacter, GridSummon } from '../grid.adapter'
import type { Weapon, Character, Summon } from '$lib/types/api/entities'
describe('GridAdapter', () => {
let adapter: GridAdapter
let originalFetch: typeof global.fetch
const mockWeapon: Weapon = {
id: 'weapon-1',
granblueId: '1040001',
name: { en: 'Test Weapon', ja: 'テスト武器' },
rarity: 5,
element: 1,
maxLevel: 150,
maxSkillLevel: 15,
maxAwakeningLevel: 5,
series: {
id: 'series-1',
slug: 'dark-opus',
name: { en: 'Dark Opus', ja: 'ダークオーパス' },
hasWeaponKeys: true,
hasAwakening: true,
augmentType: 'no_augment',
extra: false,
elementChangeable: false
},
proficiency: 1,
ax: true,
axType: 1,
uncap: { flb: true, ulb: true, transcendence: false },
hp: { minHp: 100, maxHp: 500, maxHpFlb: 600, maxHpUlb: 700 },
atk: { minAtk: 200, maxAtk: 1000, maxAtkFlb: 1200, maxAtkUlb: 1400 }
}
const mockCharacter: Character = {
id: 'char-1',
granblueId: '3040001',
name: { en: 'Test Character', ja: 'テストキャラ' },
rarity: 5,
element: 1,
maxLevel: 100,
uncap: { flb: true, ulb: true },
special: false,
recruits: null,
gender: 0,
race: { race1: 1, race2: 0 },
proficiency: [1],
hp: { minHp: 150, maxHp: 750, maxHpFlb: 900 },
atk: { minAtk: 250, maxAtk: 1250, maxAtkFlb: 1500 }
}
const mockSummon: Summon = {
id: 'summon-1',
granblueId: '2040001',
name: { en: 'Test Summon', ja: 'テスト召喚石' },
rarity: 5,
element: 1,
maxLevel: 150,
uncap: { flb: true, ulb: true, transcendence: false },
subaura: false,
hp: { minHp: 100, maxHp: 500, maxHpFlb: 600, maxHpUlb: 700 },
atk: { minAtk: 200, maxAtk: 1000, maxAtkFlb: 1200, maxAtkUlb: 1400 }
}
const mockGridWeapon: GridWeapon = {
id: 'gw-1',
position: 1,
mainhand: true,
uncapLevel: 5,
transcendenceStep: 0,
weapon: mockWeapon
}
const mockGridCharacter: GridCharacter = {
id: 'gc-1',
position: 1,
uncapLevel: 5,
transcendenceStep: 1,
character: mockCharacter
}
const mockGridSummon: GridSummon = {
id: 'gs-1',
position: 1,
quickSummon: true,
uncapLevel: 5,
transcendenceStep: 2,
summon: mockSummon
}
beforeEach(() => {
originalFetch = global.fetch
adapter = new GridAdapter({ baseURL: 'https://api.example.com' })
})
afterEach(() => {
global.fetch = originalFetch
vi.clearAllTimers()
})
describe('weapon operations', () => {
it('should create a grid weapon', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockGridWeapon
})
const result = await adapter.createWeapon({
partyId: 'party-1',
weaponId: 'weapon-1',
position: 1,
mainhand: true
})
expect(result).toEqual(mockGridWeapon)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/grid_weapons',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
party_id: 'party-1',
weapon_id: 'weapon-1',
position: 1,
mainhand: true
})
})
)
})
it('should update a grid weapon', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ ...mockGridWeapon, uncapLevel: 6 })
})
const result = await adapter.updateWeapon('gw-1', {
uncapLevel: 6
})
expect(result.uncapLevel).toBe(6)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/grid_weapons/gw-1',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ uncap_level: 6 })
})
)
})
it('should delete a grid weapon', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({})
})
await adapter.deleteWeapon({
id: 'gw-1',
partyId: 'party-1'
})
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/grid_weapons',
expect.objectContaining({
method: 'DELETE',
body: JSON.stringify({
id: 'gw-1',
party_id: 'party-1'
})
})
)
})
it('should update weapon uncap level', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ ...mockGridWeapon, uncapLevel: 6 })
})
const result = await adapter.updateWeaponUncap({
id: 'gw-1',
partyId: 'party-1',
uncapLevel: 6,
transcendenceStep: 1
})
expect(result.uncapLevel).toBe(6)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/grid_weapons/update_uncap',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
id: 'gw-1',
party_id: 'party-1',
uncap_level: 6,
transcendence_step: 1
})
})
)
})
it('should resolve weapon conflicts', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockGridWeapon
})
const result = await adapter.resolveWeaponConflict({
partyId: 'party-1',
incomingId: 'weapon-2',
position: 1,
conflictingIds: ['gw-1']
})
expect(result).toEqual(mockGridWeapon)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/grid_weapons/resolve',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
party_id: 'party-1',
incoming_id: 'weapon-2',
position: 1,
conflicting_ids: ['gw-1']
})
})
)
})
it('should update weapon position', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ ...mockGridWeapon, position: 2 })
})
const result = await adapter.updateWeaponPosition({
partyId: 'party-1',
id: 'gw-1',
position: 2
})
expect(result.position).toBe(2)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/parties/party-1/grid_weapons/gw-1/position',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ position: 2 })
})
)
})
it('should swap weapon positions', async () => {
const mockResponse = {
source: mockGridWeapon,
target: { ...mockGridWeapon, id: 'gw-2', position: 2 }
}
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockResponse
})
const result = await adapter.swapWeapons({
partyId: 'party-1',
sourceId: 'gw-1',
targetId: 'gw-2'
})
expect(result).toEqual(mockResponse)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/parties/party-1/grid_weapons/swap',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
source_id: 'gw-1',
target_id: 'gw-2'
})
})
)
})
})
describe('character operations', () => {
it('should create a grid character', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockGridCharacter
})
const result = await adapter.createCharacter({
partyId: 'party-1',
characterId: 'char-1',
position: 1
})
expect(result).toEqual(mockGridCharacter)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/grid_characters',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
party_id: 'party-1',
character_id: 'char-1',
position: 1
})
})
)
})
it('should update character position', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ ...mockGridCharacter, position: 2 })
})
const result = await adapter.updateCharacterPosition({
partyId: 'party-1',
id: 'gc-1',
position: 2
})
expect(result.position).toBe(2)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/parties/party-1/grid_characters/gc-1/position',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ position: 2 })
})
)
})
it('should swap character positions', async () => {
const mockResponse = {
source: mockGridCharacter,
target: { ...mockGridCharacter, id: 'gc-2', position: 2 }
}
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockResponse
})
const result = await adapter.swapCharacters({
partyId: 'party-1',
sourceId: 'gc-1',
targetId: 'gc-2'
})
expect(result).toEqual(mockResponse)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/parties/party-1/grid_characters/swap',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
source_id: 'gc-1',
target_id: 'gc-2'
})
})
)
})
})
describe('summon operations', () => {
it('should create a grid summon', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockGridSummon
})
const result = await adapter.createSummon({
partyId: 'party-1',
summonId: 'summon-1',
position: 1,
quickSummon: true
})
expect(result).toEqual(mockGridSummon)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/grid_summons',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
party_id: 'party-1',
summon_id: 'summon-1',
position: 1,
quick_summon: true
})
})
)
})
it('should update quick summon', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ ...mockGridSummon, quickSummon: false })
})
const result = await adapter.updateQuickSummon({
id: 'gs-1',
partyId: 'party-1',
quickSummon: false
})
expect(result.quickSummon).toBe(false)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/grid_summons/update_quick_summon',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
id: 'gs-1',
party_id: 'party-1',
quick_summon: false
})
})
)
})
it('should update summon position', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ ...mockGridSummon, position: 2 })
})
const result = await adapter.updateSummonPosition({
partyId: 'party-1',
id: 'gs-1',
position: 2
})
expect(result.position).toBe(2)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/parties/party-1/grid_summons/gs-1/position',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ position: 2 })
})
)
})
it('should swap summon positions', async () => {
const mockResponse = {
source: mockGridSummon,
target: { ...mockGridSummon, id: 'gs-2', position: 2 }
}
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockResponse
})
const result = await adapter.swapSummons({
partyId: 'party-1',
sourceId: 'gs-1',
targetId: 'gs-2'
})
expect(result).toEqual(mockResponse)
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/parties/party-1/grid_summons/swap',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
source_id: 'gs-1',
target_id: 'gs-2'
})
})
)
})
})
describe('cache management', () => {
it('should clear grid cache', () => {
const clearCacheSpy = vi.spyOn(adapter, 'clearCache')
adapter.clearGridCache('party-1')
expect(clearCacheSpy).toHaveBeenCalledWith('/parties/party-1/grid')
})
it('should clear all grid caches', () => {
const clearCacheSpy = vi.spyOn(adapter, 'clearCache')
adapter.clearGridCache()
expect(clearCacheSpy).toHaveBeenCalledWith('/grid')
})
})
describe('error handling', () => {
it('should handle 404 errors', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
json: async () => ({ error: 'Grid weapon not found' })
})
await expect(
adapter.updateWeapon('invalid-id', { uncapLevel: 5 })
).rejects.toThrow()
})
it('should handle validation errors', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 422,
statusText: 'Unprocessable Entity',
json: async () => ({
errors: {
position: ['is already taken']
}
})
})
await expect(
adapter.createWeapon({
partyId: 'party-1',
weaponId: 'weapon-1',
position: 1
})
).rejects.toThrow()
})
})
})