mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-09-16 10:32:10 +00:00
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.
This commit is contained in:
@@ -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 }) => {
|
Given('my offline copy is synchronized', async ({ page, state }) => {
|
||||||
await waitForServiceWorker(page);
|
await waitForServiceWorker(page);
|
||||||
|
// A full Living Dex snapshot plus its artwork can take longer than the default poll window on CI.
|
||||||
await expect
|
await expect
|
||||||
.poll(() =>
|
.poll(
|
||||||
page.evaluate(async (userId) => {
|
() =>
|
||||||
const meta = await (
|
page.evaluate(async (userId) => {
|
||||||
await caches.open('livingdex-offline-meta-v1')
|
const meta = await (
|
||||||
).match('/__offline/current');
|
await caches.open('livingdex-offline-meta-v1')
|
||||||
if (!meta) return false;
|
).match('/__offline/current');
|
||||||
const value = await meta.json();
|
if (!meta) return false;
|
||||||
return value.userId === userId;
|
const value = await meta.json();
|
||||||
}, state.userId)
|
return value.userId === userId;
|
||||||
|
}, state.userId),
|
||||||
|
{ timeout: 30_000 }
|
||||||
)
|
)
|
||||||
.toBe(true);
|
.toBe(true);
|
||||||
const serializedSnapshot = await page.evaluate(async () => {
|
const serializedSnapshot = await page.evaluate(async () => {
|
||||||
|
|||||||
@@ -42,6 +42,22 @@ describe('share preview rendering', () => {
|
|||||||
expect(svg).toContain('42%');
|
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}</text>`);
|
||||||
|
}
|
||||||
|
expect(svg).not.toContain('All Games');
|
||||||
|
expect(svg).not.toContain('class="description"');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders a valid 1200 by 630 PNG', async () => {
|
it('renders a valid 1200 by 630 PNG', async () => {
|
||||||
const png = await renderSharePreview(shared);
|
const png = await renderSharePreview(shared);
|
||||||
const metadata = await sharp(png).metadata();
|
const metadata = await sharp(png).metadata();
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { calculateSharedProgress, isShareToken } from '$lib/services/SharedPokedexService';
|
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', () => {
|
describe('shared Pokédex helpers', () => {
|
||||||
it('accepts UUID capability tokens and rejects malformed route values', () => {
|
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 });
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user