diff --git a/tests/bdd/steps/pwa.steps.ts b/tests/bdd/steps/pwa.steps.ts index e38cafe..4ec42f6 100644 --- a/tests/bdd/steps/pwa.steps.ts +++ b/tests/bdd/steps/pwa.steps.ts @@ -67,16 +67,19 @@ When('I go offline and reload the sign-in page', async ({ page }) => { Given('my offline copy is synchronized', async ({ page, state }) => { await waitForServiceWorker(page); + // A full Living Dex snapshot plus its artwork can take longer than the default poll window on CI. await expect - .poll(() => - page.evaluate(async (userId) => { - const meta = await ( - await caches.open('livingdex-offline-meta-v1') - ).match('/__offline/current'); - if (!meta) return false; - const value = await meta.json(); - return value.userId === userId; - }, state.userId) + .poll( + () => + page.evaluate(async (userId) => { + const meta = await ( + await caches.open('livingdex-offline-meta-v1') + ).match('/__offline/current'); + if (!meta) return false; + const value = await meta.json(); + return value.userId === userId; + }, state.userId), + { timeout: 30_000 } ) .toBe(true); const serializedSnapshot = await page.evaluate(async () => { diff --git a/tests/unit/sharePreviewService.test.ts b/tests/unit/sharePreviewService.test.ts index 09d1530..001beb8 100644 --- a/tests/unit/sharePreviewService.test.ts +++ b/tests/unit/sharePreviewService.test.ts @@ -42,6 +42,22 @@ describe('share preview rendering', () => { expect(svg).toContain('42%'); }); + it('lists every enabled dex badge and omits an empty description', () => { + const svg = buildSharePreviewSvg({ + ...shared, + description: '', + isShinyDex: true, + isOriginDex: true, + isFormDex: true, + gameScope: 'Scarlet' + }); + for (const badge of ['Living', 'Shiny', 'Origin', 'Form', 'Scarlet']) { + expect(svg).toContain(`class="badge">${badge}`); + } + expect(svg).not.toContain('All Games'); + expect(svg).not.toContain('class="description"'); + }); + it('renders a valid 1200 by 630 PNG', async () => { const png = await renderSharePreview(shared); const metadata = await sharp(png).metadata(); diff --git a/tests/unit/sharedPokedexService.test.ts b/tests/unit/sharedPokedexService.test.ts index 24c4097..5b5cc49 100644 --- a/tests/unit/sharedPokedexService.test.ts +++ b/tests/unit/sharedPokedexService.test.ts @@ -1,5 +1,25 @@ -import { describe, expect, it } from 'vitest'; -import { calculateSharedProgress, isShareToken } from '$lib/services/SharedPokedexService'; +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', () => { @@ -32,3 +52,75 @@ describe('shared Pokédex helpers', () => { 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 + }); + }); +});