Files
LivingDexTracker/tests/unit/sharedPokedexService.test.ts
T
Josh Creek ca7f9c0e48 test(pokedex): cover shared dex loading and preview badges
Adds loadSharedPokedex unit tests and a share preview case for every badge, restoring the branch coverage gate. Gives the offline sync BDD poll 30s so a full Living Dex snapshot can finish caching on CI.
2026-09-14 14:47:27 +01:00

127 lines
3.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { PokedexEntry } from '$lib/models/PokedexEntry';
import type { SharedPokedexRpcData } from '$lib/models/SharedPokedex';
const { findAllCombinedData } = vi.hoisted(() => ({ findAllCombinedData: vi.fn() }));
vi.mock('$lib/repositories/CombinedDataRepository', () => ({
default: vi.fn().mockImplementation(() => ({ findAllCombinedData }))
}));
import {
calculateSharedProgress,
isShareToken,
loadSharedPokedex
} from '$lib/services/SharedPokedexService';
const TOKEN = '123e4567-e89b-42d3-a456-426614174000';
function supabaseWithRpc(result: { data: unknown; error: unknown }) {
const rpc = vi.fn().mockResolvedValue(result);
return { supabase: { rpc } as unknown as SupabaseClient, rpc };
}
describe('shared Pokédex helpers', () => {
it('accepts UUID capability tokens and rejects malformed route values', () => {
expect(isShareToken('123e4567-e89b-42d3-a456-426614174000')).toBe(true);
expect(isShareToken('not-a-token')).toBe(false);
expect(isShareToken('123e4567-e89b-12d3-a456-426614174000/extra')).toBe(false);
});
it('counts caught and needs-to-evolve entries as progress', () => {
const progress = calculateSharedProgress(
[
{
pokemonId: '1',
caught: true,
haveToEvolve: false,
inHome: false,
hasGigantamaxed: false
},
{
pokemonId: '2',
caught: false,
haveToEvolve: true,
inHome: false,
hasGigantamaxed: false
}
],
4
);
expect(progress).toEqual({ caught: 2, completionPercentage: 50 });
expect(calculateSharedProgress([], 0)).toEqual({ caught: 0, completionPercentage: 0 });
});
});
describe('loadSharedPokedex', () => {
beforeEach(() => {
findAllCombinedData.mockReset();
});
it('rejects malformed tokens without querying', async () => {
const { supabase, rpc } = supabaseWithRpc({ data: null, error: null });
expect(await loadSharedPokedex(supabase, 'not-a-token')).toBeNull();
expect(rpc).not.toHaveBeenCalled();
});
it('returns null when the token matches nothing or the RPC fails', async () => {
expect(
await loadSharedPokedex(supabaseWithRpc({ data: null, error: null }).supabase, TOKEN)
).toBeNull();
expect(
await loadSharedPokedex(
supabaseWithRpc({ data: null, error: { message: 'boom' } }).supabase,
TOKEN
)
).toBeNull();
expect(findAllCombinedData).not.toHaveBeenCalled();
});
it('maps shared catch statuses onto the scoped dex entries', async () => {
const rpcData: SharedPokedexRpcData = {
name: 'Kanto',
description: 'Gen 1',
isLivingDex: true,
isShinyDex: false,
isOriginDex: false,
isFormDex: true,
gameScope: null,
dexScopes: ['kanto'],
catchStatuses: [
{ pokemonId: '1', caught: true, haveToEvolve: false, inHome: true, hasGigantamaxed: false },
{
pokemonId: '99',
caught: true,
haveToEvolve: false,
inHome: false,
hasGigantamaxed: false
}
]
};
const entries = ['1', '2', '3', '4'].map((id) => ({
pokedexEntry: { _id: id } as PokedexEntry,
catchRecord: null
}));
findAllCombinedData.mockResolvedValue(entries);
const { supabase, rpc } = supabaseWithRpc({ data: rpcData, error: null });
const shared = await loadSharedPokedex(supabase, TOKEN);
expect(rpc).toHaveBeenCalledWith('get_shared_pokedex', { p_share_token: TOKEN });
expect(findAllCombinedData).toHaveBeenCalledWith('', true, '', '', ['kanto']);
expect(shared?.combinedData.map((entry) => entry.catchRecord?.pokemonId ?? null)).toEqual([
'1',
null,
null,
null
]);
// Statuses for Pokémon outside the dex scope must not inflate progress.
expect(shared).toMatchObject({
name: 'Kanto',
total: 4,
caught: 1,
completionPercentage: 25
});
});
});