import { test, expect, type APIRequestContext } from '@playwright/test'; import postgres from 'postgres'; test.skip( !process.env.TRACKER_E2E, 'Opt-in: npm run test:tracker uses the migrated local development database.' ); const origin = 'http://127.0.0.1:4174'; const users: string[] = []; let sql: ReturnType; test.beforeAll(() => { if (!process.env.DB_URL) throw new Error('DB_URL required'); sql = postgres(process.env.DB_URL, { max: 1 }); }); test.afterAll(async () => { if (!sql) return; for (const id of users) await sql`delete from "user" where id=${id}`; await sql.end(); }); async function register(request: APIRequestContext, attempt = 0): Promise { const response = await request.post('/api/auth/sign-up/email', { headers: { Origin: origin }, data: { name: 'Tracker Tester', email: `tracker-${crypto.randomUUID()}@example.com`, password: 'Tracker-test-password-2026!' } }); if (response.status() === 429 && attempt < 2) { await new Promise((resolve) => setTimeout(resolve, 11000)); return register(request, attempt + 1); } expect(response.ok(), await response.text()).toBe(true); const body = await response.json(); users.push(body.user.id); return body.user.id as string; } async function action( request: APIRequestContext, path: string, values: Record ) { const body = new URLSearchParams(); for (const [k, v] of Object.entries(values)) for (const value of Array.isArray(v) ? v : [v]) body.append(k, value); const response = await request.post(path, { headers: { Origin: origin, Accept: 'application/json', 'x-sveltekit-action': 'true', 'content-type': 'application/x-www-form-urlencoded' }, data: body.toString() }); const result = await response.json(); return { status: result.status ?? response.status(), type: result.type, result }; } test('Platinum route, regional dex, catch journal, duplicate policy, team slots and ownership', async ({ page, playwright }) => { const id = await register(page.request); const [{ id: gameId }] = await sql`select id from games where slug='platinum'`; const [{ id: pokemonId }] = await sql`select id from pokemon where number=387`; await page.goto('/projects/game-tracker/pokemon?game=platinum'); await expect(page.getByRole('heading', { name: 'Pokémon journal' })).toBeVisible(); await expect(page.getByRole('button', { name: 'set rules', exact: true })).toBeEnabled({ timeout: 15000 }); await expect(page.getByRole('button', { name: /Turtwig Grass/ })).toBeVisible(); await expect(page.getByRole('button', { name: /Nidoran/ })).toHaveCount(0); await page.getByRole('button', { name: 'set rules', exact: true }).click(); await expect(page.getByRole('dialog', { name: 'Set rules' })).toBeVisible(); await page.keyboard.press('Escape'); await expect(page.getByRole('dialog')).toHaveCount(0); await page.getByRole('tab', { name: 'Pokédex', exact: true }).click(); await expect(page.getByLabel('Pokédex order')).toHaveValue('extended-sinnoh'); await expect(page.locator('article').first()).toContainText('Turtwig'); await page.getByLabel('Pokédex order').selectOption('national'); await expect(page.locator('article').first()).toContainText('Bulbasaur'); const catchValues = { gameId: String(gameId), pokemonId: String(pokemonId), nickname: 'Sprout', level: '5', moves: ['Tackle', 'Withdraw'], stage: 'story', obtained: 'starter', partySlot: '1', caughtOn: '2026-09-25', location: 'Route 201' }; expect( (await action(page.request, '/projects/game-tracker/pokemon?/saveCatch', catchValues)).status ).toBe(200); const [first] = await sql`select * from pokemon_catches where user_id=${id}`; expect( ( await action(page.request, '/projects/game-tracker/pokemon?/saveCatch', { ...catchValues, nickname: 'Second Sprout', partySlot: '2' }) ).status ).toBe(200); const [second] = await sql`select * from pokemon_catches where user_id=${id} and id<>${first.id}`; await page.reload(); await page.getByRole('tab', { name: /My PC/ }).click(); await expect(page.getByRole('tabpanel').getByRole('button', { name: /#387/ })).toContainText( 'Sprout' ); await expect(page.getByRole('tabpanel').getByRole('button', { name: /#387/ })).not.toContainText( 'Second Sprout' ); await page.getByRole('tabpanel').getByRole('button', { name: /#387/ }).click(); await expect(page.getByRole('dialog')).toContainText('Meet Sprout'); await page.getByLabel('Level', { exact: true }).fill('12'); await page.getByRole('button', { name: 'Save Pokémon', exact: true }).click(); await expect(page.getByRole('dialog')).toHaveCount(0); expect((await sql`select level from pokemon_catches where id=${first.id}`)[0].level).toBe(12); expect( ( await action(page.request, '/projects/game-tracker/pokemon?/saveCatch', { ...catchValues, id: String(second.id), nickname: 'Second Sprout', partySlot: '1' }) ).status ).toBe(200); expect( (await sql`select party_slot from pokemon_catches where id=${first.id}`)[0].party_slot ).toBeNull(); expect( ( await action(page.request, '/projects/game-tracker/pokemon?/saveCatch', { ...catchValues, moves: ['a', 'b', 'c', 'd', 'e'] }) ).status ).toBe(400); const other = await playwright.request.newContext({ baseURL: origin }); await register(other); expect( ( await action(other, '/projects/game-tracker/pokemon?/saveCatch', { ...catchValues, id: String(first.id) }) ).status ).toBe(404); expect( (await action(other, '/projects/game-tracker/pokemon?/deleteCatch', { id: String(first.id) })) .status ).toBe(404); await other.dispose(); expect( ( await action(page.request, '/projects/game-tracker/pokemon?/saveJourney', { gameId: String(gameId), stage: 'postgame', location: 'stark-mountain' }) ).status ).toBe(200); expect( ( await action(page.request, '/projects/game-tracker/pokemon?/saveMilestone', { gameId: String(gameId), stage: 'story', key: 'league:champion-cynthia', title: 'Champion · Cynthia', category: 'league', done: 'on' }) ).status ).toBe(200); await page.reload(); await expect(page.getByLabel('My playthrough')).toHaveValue('postgame'); await expect(page.getByLabel('Encounter location')).toHaveValue('stark-mountain'); await page.screenshot({ path: '/tmp/tracker-pokemon-saved.png', fullPage: true }); const [entry] = await sql`select id from pokemon_game_entries where game_id=${gameId} and pokemon_id=${pokemonId}`; expect( ( await action(page.request, '/projects/game-tracker/pokemon?/togglePokemonCaught', { entryId: String(entry.id) }) ).status ).toBe(400); for (const row of [first, second]) expect( ( await action(page.request, '/projects/game-tracker/pokemon?/deleteCatch', { id: String(row.id) }) ).status ).toBe(200); expect( ( await sql`select * from user_entries where user_id=${id} and pokemon_game_entry_id=${entry.id}` ).length ).toBe(1); expect( ( await action(page.request, '/projects/game-tracker/pokemon?/togglePokemonCaught', { entryId: String(entry.id) }) ).status ).toBe(200); }); test('weapon/player ranks, challenge progress and osu goals/scores/training persist independently', async ({ page, playwright }) => { const id = await register(page.request); const [weapon] = await sql`select i.id, i.name from tracker_items i join tracker_games g on i.game_id=g.id where g.slug='black-ops-3' and i.kind='weapon' order by i.id limit 1`; const [challenge] = await sql`select c.id,c.target from tracker_challenges c join tracker_games g on c.game_id=g.id where g.slug='black-ops-3' and c.target>1 limit 1`; const cod = '/projects/game-tracker/call-of-duty/black-ops-3'; await page.goto(cod); await page.getByRole('button', { name: 'Update my rank' }).click(); await page.getByLabel('Current level', { exact: true }).fill('42'); await page.getByLabel('Prestige', { exact: true }).fill('3'); await page.getByRole('button', { name: 'Save rank' }).click(); await expect(page.getByRole('dialog')).toHaveCount(0); await expect(page.getByLabel('Player level and prestige')).toContainText('LEVEL42'); await expect(page.getByLabel('Player level and prestige')).toContainText('PRESTIGE3'); expect( ( await action(page.request, cod + '?/saveRank', { itemId: String(weapon.id), level: '17', prestige: '2' }) ).status ).toBe(200); expect( ( await action(page.request, cod + '?/saveChallenge', { challengeId: String(challenge.id), current: String(challenge.target) }) ).status ).toBe(200); await page.reload(); const weaponCard = page.getByRole('article', { name: weapon.name, exact: true }); await expect( weaponCard.getByRole('button', { name: `Edit ${weapon.name} level and prestige` }) ).toContainText('17'); await weaponCard.getByRole('button', { name: `${weapon.name} camos and milestones` }).click(); await expect(page.getByRole('dialog')).toContainText('Max level reached'); await page.keyboard.press('Escape'); await page.getByRole('tab', { name: 'Challenges', exact: true }).click(); await expect(page.locator('article')).toHaveCount(36); const goal = { kind: 'goal', title: 'First 200 BPM FC', mode: 'osu', goal: 'fc', skill: 'stamina', playedOn: '2026-09-25', rate: '1.00', mods: 'NM', beatmapUrl: 'https://osu.ppy.sh/beatmaps/123' }; expect((await action(page.request, '/projects/game-tracker/osu?/save', goal)).status).toBe(200); expect( ( await action(page.request, '/projects/game-tracker/osu?/save', { ...goal, kind: 'score', title: 'My best play', accuracy: '99.25', combo: '1234', misses: '0', pp: '250.3', scoreUrl: 'https://osu.ppy.sh/scores/123' }) ).status ).toBe(200); expect( ( await action(page.request, '/projects/game-tracker/osu?/save', { ...goal, kind: 'training', title: 'Stream practice', bpm: '180', duration: '60', accuracy: '98.5' }) ).status ).toBe(200); expect( ( await action(page.request, '/projects/game-tracker/osu?/save', { ...goal, kind: 'training', title: 'Faster streams', bpm: '140', duration: '45', rate: '1.50', mods: 'DT', accuracy: '97.5' }) ).status ).toBe(200); expect( ( await action(page.request, '/projects/game-tracker/osu?/save', { ...goal, beatmapUrl: 'https://osu.ppy.sh.evil.test/beatmaps/123' }) ).status ).toBe(400); expect( ( await action(page.request, '/projects/game-tracker/osu?/save', { ...goal, kind: 'training', duration: '0', bpm: '180' }) ).status ).toBe(400); await page.goto('/projects/game-tracker/osu'); await expect(page.getByRole('heading', { name: 'First 200 BPM FC' })).toBeVisible(); await page.getByRole('button', { name: 'Achieved!', exact: true }).click(); await expect(page.getByText('✓ achieved')).toBeVisible(); await page.getByRole('tab', { name: 'Training log' }).click(); await expect(page.getByText('210 effective BPM')).toBeVisible(); await expect(page.getByRole('img', { name: /Last 2 qualifying/ })).toBeVisible(); await page.screenshot({ path: '/tmp/tracker-osu-saved.png', fullPage: true }); const [row] = await sql`select id from osu_entries where user_id=${id} limit 1`; const other = await playwright.request.newContext({ baseURL: origin }); await register(other); expect( (await action(other, '/projects/game-tracker/osu?/save', { ...goal, id: String(row.id) })) .status ).toBe(404); expect( (await action(other, '/projects/game-tracker/osu?/delete', { id: String(row.id) })).status ).toBe(404); await other.dispose(); }); test('shared dialogs have focus, Escape dismissal and mobile layouts without overflow', async ({ page }) => { const id = await register(page.request); await sql`update "user" set role='admin' where id=${id}`; for (const [path, label, title] of [ ['/blog', 'new article', 'new article'], ['/lists', 'new list', 'new list'] ]) { await page.goto(path); await page.getByRole('button', { name: label, exact: true }).click(); const dialog = page.getByRole('dialog', { name: title }); await expect(dialog).toBeVisible(); await expect(dialog.getByRole('textbox').first()).toBeFocused(); await page.keyboard.press('Escape'); await expect(dialog).toHaveCount(0); await expect(page.getByRole('button', { name: label, exact: true })).toBeFocused(); } await page.goto('/projects/money-tracker'); await page.getByRole('heading', { name: 'Accounts', exact: true }).waitFor(); await page.locator('#accounts').getByRole('button', { name: 'Add', exact: true }).click(); const moneyDialog = page.getByRole('dialog'); await expect(moneyDialog).toBeVisible(); await moneyDialog.getByLabel('Account name', { exact: true }).fill('Tracker integration account'); await moneyDialog.getByLabel(/^Opening balance/).fill('123.45'); await moneyDialog.getByRole('button', { name: 'Save', exact: true }).click(); await expect(moneyDialog).toHaveCount(0); expect( (await sql`select opening_balance_cents from money_accounts where user_id=${id}`)[0] .opening_balance_cents ).toBe(12345); await page.setViewportSize({ width: 390, height: 844 }); for (const path of [ '/projects/game-tracker', '/projects/game-tracker/pokemon?game=platinum', '/projects/game-tracker/osu', '/projects/game-tracker/call-of-duty/black-ops-3' ]) { await page.goto(path); await expect(page.locator('h1')).not.toHaveText('500'); expect( await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth) ).toBe(true); } await page.goto('/projects/game-tracker/pokemon?game=platinum'); await expect(page.getByRole('button', { name: /Turtwig Grass/ })).toBeVisible(); await page.screenshot({ path: '/tmp/tracker-pokemon-mobile.png', fullPage: true }); }); test('illustrated library uses two Pokémon columns, follows selected games, and picture buttons select one game', async ({ page }) => { await register(page.request); const games = await sql<{ id: number; slug: string }[]>`select id, slug from games`; const selected = ['platinum', 'red', 'white', 'emerald', 'legends-arceus']; expect( ( await action(page.request, '/projects/game-tracker/pokemon?/setGames', { gameIds: games.filter((g) => selected.includes(g.slug)).map((g) => String(g.id)) }) ).status ).toBe(200); await page.setViewportSize({ width: 1440, height: 900 }); await page.goto('/projects/game-tracker'); await expect(page.locator('.collection-grid .game-row')).toHaveCount(5); await expect(page.locator('.collection-grid .game-name')).toHaveText([ 'Red', 'Emerald', 'Platinum', 'White', 'Legends: Arceus' ]); await expect(page.locator('.cod-rows .game-row')).toHaveCount(7); await expect .poll(() => page .locator('main img') .evaluateAll((images) => images.every( (i) => (i as HTMLImageElement).complete && (i as HTMLImageElement).naturalWidth > 0 ) ) ) .toBe(true); await page.screenshot({ path: '/tmp/tracker-library-pictures.png', fullPage: true }); await page.getByRole('link', { name: /Platinum artwork Platinum/ }).click(); await expect(page.getByRole('button', { name: 'Play Platinum', exact: true })).toHaveAttribute( 'aria-pressed', 'true' ); await expect(page.getByRole('combobox', { name: 'Current game' })).toHaveCount(0); await expect(page.getByRole('dialog')).toHaveCount(0); const map = page.getByRole('img', { name: 'Sinnoh region map from the Pokémon games' }); await expect(map).toBeVisible(); await expect .poll(() => map.evaluate((i) => (i as HTMLImageElement).naturalWidth)) .toBeGreaterThan(0); await page.getByRole('button', { name: 'Explore Route 201', exact: true }).click(); await expect(page.getByRole('button', { name: /Turtwig Grass/ })).toBeVisible(); await map.scrollIntoViewIfNeeded(); await page.screenshot({ path: '/tmp/tracker-real-map.png' }); await page.getByRole('button', { name: 'Play White', exact: true }).click(); await expect(page).toHaveURL(/game=white/); await expect(page.getByRole('button', { name: 'Play White', exact: true })).toHaveAttribute( 'aria-pressed', 'true' ); await expect(page.getByRole('button', { name: 'Play Platinum', exact: true })).toHaveAttribute( 'aria-pressed', 'false' ); // Large collections stay readable in exactly two columns; vertical page scrolling is allowed. expect( ( await action(page.request, '/projects/game-tracker/pokemon?/setGames', { gameIds: games.map((g) => String(g.id)) }) ).status ).toBe(200); for (const [width, height] of [ [1440, 900], [1280, 720] ]) { await page.setViewportSize({ width, height }); await page.goto('/projects/game-tracker'); await expect(page.locator('.collection-grid .game-row')).toHaveCount(games.length); const size = await page.evaluate(() => ({ height: document.documentElement.scrollHeight, width: document.documentElement.scrollWidth, viewportHeight: innerHeight, viewportWidth: innerWidth })); expect( await page .locator('.collection-grid') .evaluate((el) => getComputedStyle(el).gridTemplateColumns.split(' ').length) ).toBe(2); expect(size.width).toBeLessThanOrEqual(size.viewportWidth); const clipped = await page.locator('.game-row').evaluateAll( (rows) => rows.filter((r) => { const b = r.getBoundingClientRect(); return b.right > innerWidth || b.height < 56; }).length ); expect(clipped).toBe(0); } await page.screenshot({ path: '/tmp/tracker-library-all-games.png', fullPage: true }); await page.goto('/projects/game-tracker/pokemon?game=platinum'); await expect( page.getByRole('group', { name: 'Choose Pokémon game' }).getByRole('button') ).toHaveCount(games.length); await expect .poll(() => page .locator('.game-cover-button') .evaluateAll( (buttons) => new Set(buttons.map((b) => Math.round(b.getBoundingClientRect().top))).size ) ) .toBe(2); await expect(page.getByRole('button', { name: 'Switch game', exact: true })).toHaveCount(0); const covers = await page.locator('.game-cover-button').evaluateAll((buttons) => buttons.every((b) => { const r = b.getBoundingClientRect(); return r.left >= 0 && r.right <= innerWidth; }) ); expect(covers).toBe(true); }); test('live weapon controls save immediately, infer max level from prestige, and handle concurrent updates', async ({ page, playwright }) => { const userId = await register(page.request); const [weapon] = await sql`select i.id,i.name from tracker_items i join tracker_games g on i.game_id=g.id where g.slug='black-ops-3' and i.kind='weapon' order by i.sort_order limit 1`; const cod = '/projects/game-tracker/call-of-duty/black-ops-3'; await page.setViewportSize({ width: 1280, height: 800 }); await page.goto(cod); await page.getByRole('button', { name: `${weapon.name} camos and milestones` }).click(); const card = page.getByRole('dialog', { name: weapon.name, exact: true }); const level = card.getByRole('button', { name: `Edit ${weapon.name} level and prestige` }); await expect(card.getByRole('button', { name: `Level down ${weapon.name}` })).toBeDisabled(); await card.getByRole('button', { name: `Level up ${weapon.name}` }).click(); await expect(level).toHaveText(/LEVEL2/); await card.getByRole('button', { name: `Level down ${weapon.name}` }).click(); await expect(level).toHaveText(/LEVEL1/); await card.getByRole('button', { name: `Prestige up ${weapon.name}` }).click(); await expect(card).toContainText('Max level reached'); await expect(card.getByRole('checkbox', { name: /max level/ })).toHaveCount(0); expect( ( await sql`select level,prestige from tracker_weapon_ranks where user_id=${userId} and item_id=${weapon.id}` )[0] ).toMatchObject({ level: 1, prestige: 1 }); const concurrent = await Promise.all( Array.from({ length: 4 }, () => action(page.request, cod + '?/adjustWeaponRank', { itemId: String(weapon.id), field: 'level', delta: '1' }) ) ); expect(concurrent.every((r) => r.status === 200)).toBe(true); await page.reload(); await page.getByRole('button', { name: `${weapon.name} camos and milestones` }).click(); await expect(level).toHaveText(/LEVEL5/); await page.route('**/*adjustWeaponRank', (route) => route.abort()); await card.getByRole('button', { name: `Level up ${weapon.name}` }).click(); await expect(card.getByRole('status')).toContainText(/fetch|save|network/i); await expect(level).toHaveText(/LEVEL5/); await page.unroute('**/*adjustWeaponRank'); await card.getByRole('button', { name: `Level up ${weapon.name}` }).click(); await expect(level).toHaveText(/LEVEL6/); expect( ( await action(page.request, cod + '?/adjustWeaponRank', { itemId: String(weapon.id), field: 'level', delta: '999' }) ).status ).toBe(400); expect( ( await action(page.request, '/projects/game-tracker/call-of-duty/wwii?/adjustWeaponRank', { itemId: String(weapon.id), field: 'level', delta: '1' }) ).status ).toBe(404); const anonymous = await playwright.request.newContext({ baseURL: origin }); expect( ( await action(anonymous, cod + '?/adjustWeaponRank', { itemId: String(weapon.id), field: 'level', delta: '1' }) ).status ).toBe(401); await anonymous.dispose(); await page.keyboard.press('Escape'); const cards = await page .locator('.weapon-card') .evaluateAll((cards) => cards.map((c) => c.getBoundingClientRect().bottom)); expect(Math.max(...cards)).toBeLessThanOrEqual(800); await page.screenshot({ path: '/tmp/tracker-live-cod.png' }); }); test('crowded Pokémon areas show every species without a scrolling list and allow a catch from the map', async ({ page }) => { await register(page.request); await page.setViewportSize({ width: 1280, height: 800 }); await page.goto('/projects/game-tracker/pokemon?game=platinum'); await expect(page.getByRole('button', { name: /Turtwig Grass/ })).toBeVisible(); const [{ id: locationId }] = await sql`select id from pokemon_locations where slug='sinnoh-route-210'`; const response = page.waitForResponse((r) => { const url = new URL(r.url()); return ( url.pathname.endsWith('/api/encounters') && url.searchParams.get('location') === String(locationId) ); }); await page.getByLabel('Encounter location', { exact: true }).selectOption('sinnoh-route-210'); const rows = await (await response).json(); const special = page.getByRole('checkbox', { name: /Include special conditions/ }); if (await special.count()) await special.check(); const expected = new Set(rows.map((r: { number: number }) => r.number)).size; const overview = page.getByRole('group', { name: 'All Pokémon in this area' }); await expect(overview.getByRole('button')).toHaveCount(expected); expect(expected).toBeGreaterThan(20); const bounds = await overview.evaluate((el) => { const parent = el.getBoundingClientRect(); return { height: el.clientHeight, scroll: el.scrollHeight, fits: [...el.querySelectorAll('button')].every((b) => { const r = b.getBoundingClientRect(); return ( r.bottom <= parent.bottom + 1 && r.right <= parent.right + 1 && r.width >= 20 && r.height >= 20 ); }) }; }); expect(bounds.scroll).toBeLessThanOrEqual(bounds.height + 1); expect(bounds.fits).toBe(true); // The overview itself always fits without a scrolling list. A large cover library // can move the workspace below the fold, so bring that panel into view first. await overview.scrollIntoViewIfNeeded(); await expect .poll(() => overview.evaluate((el) => el.getBoundingClientRect().bottom <= innerHeight)) .toBe(true); await page.screenshot({ path: '/tmp/tracker-compact-pokemon.png' }); await page.getByRole('button', { name: 'Expand encounters' }).click(); const expanded = page.getByRole('dialog'); await expect(expanded.getByRole('group').getByRole('button')).toHaveCount(expected); await expanded.getByRole('group').getByRole('button').first().click(); const details = page.getByRole('dialog').last(); await expect(details).toContainText('Lv.'); await details.getByRole('button', { name: 'Mark as seen', exact: true }).click(); await expect(details.getByRole('button', { name: '✓ Seen · undo', exact: true })).toBeEnabled(); await details.getByRole('button', { name: 'Register a catch', exact: true }).click(); const editor = page.getByRole('dialog', { name: /^Register / }); await expect(editor).toContainText('Add to my collection'); await expect(page.locator('[role="dialog"][data-state="closed"]')).toHaveCount(0); await editor.getByLabel('Nickname', { exact: true }).fill('Route companion'); await editor.getByRole('button', { name: 'Add to my collection', exact: true }).click(); await expect(page.getByRole('dialog')).toHaveCount(0); await expect(overview.getByRole('button').first()).toHaveAttribute('aria-label', /caught/); for (const [width, height] of [ [1280, 720], [768, 1024] ]) { await page.setViewportSize({ width, height }); await expect(overview.getByRole('button')).toHaveCount(expected); await overview.scrollIntoViewIfNeeded(); await expect .poll(() => overview.evaluate((el) => el.getBoundingClientRect().bottom <= innerHeight)) .toBe(true); } await page.setViewportSize({ width: 390, height: 844 }); await expect(page.getByRole('tab', { name: 'Area Pokémon', exact: true })).toBeVisible(); await expect(overview.getByRole('button')).toHaveCount(expected); await overview.scrollIntoViewIfNeeded(); await expect .poll(() => overview.evaluate((el) => el.getBoundingClientRect().bottom <= innerHeight)) .toBe(true); await page.screenshot({ path: '/tmp/tracker-crowded-mobile.png' }); await expect( page.getByRole('img', { name: 'Sinnoh region map from the Pokémon games' }) ).not.toBeVisible(); await page.getByRole('tab', { name: 'Region map', exact: true }).click(); await page.getByRole('button', { name: 'Explore Route 201', exact: true }).click(); await expect(page.getByRole('button', { name: /Turtwig Grass/ })).toBeVisible(); await page.screenshot({ path: '/tmp/tracker-live-mobile.png' }); }); test('visible progress reacts to sightings, catches and completed stories without double-counting species', async ({ page, playwright }) => { const userId = await register(page.request); const games = await sql< { id: number; slug: string }[] >`select id,slug from games where slug in ('platinum','diamond')`; const platinum = games.find((g) => g.slug === 'platinum')!; const diamond = games.find((g) => g.slug === 'diamond')!; const [species] = await sql`select id from pokemon where number=387`; await action(page.request, '/projects/game-tracker/pokemon?/setGames', { gameIds: games.map((g) => String(g.id)) }); await page.setViewportSize({ width: 1440, height: 900 }); await page.goto('/projects/game-tracker/pokemon?game=platinum'); const collection = page.getByRole('progressbar', { name: 'National collection', exact: true }); const sightings = page.getByRole('progressbar', { name: 'Species seen at least once', exact: true }); const completed = page.getByRole('progressbar', { name: 'Games completed', exact: true }); const gameProgress = page.getByRole('progressbar', { name: 'Platinum completion', exact: true }); await expect(collection).toHaveAttribute('aria-valuenow', '0'); await page.getByRole('tab', { name: 'Pokédex', exact: true }).click(); await page.getByRole('checkbox', { name: 'Seen Turtwig in Platinum', exact: true }).check(); await expect .poll(async () => Number(await sightings.getAttribute('aria-valuenow'))) .toBeGreaterThan(0); await expect(collection).toHaveAttribute('aria-valuenow', '0'); await page.reload(); await expect .poll(async () => Number(await sightings.getAttribute('aria-valuenow'))) .toBeGreaterThan(0); await page.getByRole('tab', { name: 'Pokédex', exact: true }).click(); await page.getByRole('checkbox', { name: 'Caught Turtwig in Platinum', exact: true }).check(); await expect .poll(async () => Number(await collection.getAttribute('aria-valuenow'))) .toBeGreaterThan(0); const first = await collection.getAttribute('aria-valuenow'); const [entry] = await sql`select id from pokemon_game_entries where game_id=${diamond.id} and pokemon_id=${species.id}`; await action(page.request, '/projects/game-tracker/pokemon?/togglePokemonCaught', { entryId: String(entry.id), caught: 'on' }); await page.reload(); await expect(collection).toHaveAttribute('aria-valuenow', first!); const before = Number(await gameProgress.getAttribute('aria-valuenow')); await page.getByRole('button', { name: 'Mark story complete', exact: true }).click(); await expect(completed).toHaveAttribute('aria-valuenow', '50'); await expect .poll(async () => Number(await gameProgress.getAttribute('aria-valuenow'))) .toBeGreaterThan(before); await page.getByRole('tab', { name: 'Badges & milestones', exact: true }).click(); await page.getByRole('button', { name: 'Complete Coal · Roark', exact: true }).click(); await expect( page.getByRole('progressbar', { name: 'Badges & trials', exact: true }) ).toHaveAttribute('aria-valuenow', '12.5'); await page.getByRole('button', { name: 'Full breakdown', exact: true }).click(); await expect( page.getByRole('dialog', { name: 'Your Pokémon progress' }).getByRole('progressbar') ).toHaveCount(16); await page.screenshot({ path: '/tmp/tracker-progress-pokemon.png' }); await page.keyboard.press('Escape'); await action(page.request, '/projects/game-tracker/pokemon?/saveJourney', { gameId: String(platinum.id), stage: 'postgame', location: 'sinnoh-route-201' }); expect( ( await sql`select completed from pokemon_journeys where user_id=${userId} and game_id=${platinum.id}` )[0].completed ).toBe(true); const unseen = await action(page.request, '/projects/game-tracker/pokemon?/setSeen', { gameId: String(platinum.id), pokemonId: String(species.id), seen: 'off' }); expect(unseen.status).toBe(400); const anonymous = await playwright.request.newContext({ baseURL: origin }); expect( ( await action(anonymous, '/projects/game-tracker/pokemon?/setSeen', { gameId: String(platinum.id), pokemonId: String(species.id), seen: 'on' }) ).status ).toBe(401); await anonymous.dispose(); await page.goto('/projects/game-tracker'); await expect( page.getByRole('progressbar', { name: 'National collection', exact: true }) ).toHaveAttribute('aria-valuenow', first!); await expect( page.getByRole('progressbar', { name: 'Pokémon games completed', exact: true }) ).toHaveAttribute('aria-valuenow', '50'); await page.screenshot({ path: '/tmp/tracker-progress-index.png', fullPage: true }); }); test('partial COD challenges and osu goals visibly advance completion meters', async ({ page }) => { await register(page.request); const cod = '/projects/game-tracker/call-of-duty/black-ops-3'; const [challenge] = await sql`select c.* from tracker_challenges c join tracker_games g on g.id=c.game_id where g.slug='black-ops-3' and c.target>=10 order by c.id limit 1`; await page.goto(cod); const overall = page.getByRole('progressbar', { name: 'Game completion', exact: true }); await expect(overall).toHaveAttribute('aria-valuenow', '0'); await page.getByRole('tab', { name: 'Challenges', exact: true }).click(); await page.getByLabel('Search challenges').fill(challenge.name); const card = page .getByRole('article') .filter({ has: page.getByRole('heading', { name: challenge.name, exact: true }) }); await card.getByRole('spinbutton').fill(String(Math.floor(challenge.target / 2))); await card.getByRole('button', { name: 'Save', exact: true }).click(); await expect .poll(async () => Number(await overall.getAttribute('aria-valuenow'))) .toBeGreaterThan(0); const partial = Number(await overall.getAttribute('aria-valuenow')); await card.getByRole('checkbox', { name: 'Unlocked' }).check(); await card.getByRole('button', { name: 'Save', exact: true }).click(); await expect .poll(async () => Number(await overall.getAttribute('aria-valuenow'))) .toBeGreaterThan(partial); await page.getByRole('tab', { name: 'Weapons', exact: true }).click(); await page.screenshot({ path: '/tmp/tracker-progress-cod.png' }); await action(page.request, '/projects/game-tracker/osu?/save', { kind: 'goal', title: 'Progress test FC', mode: 'osu', goal: 'fc', skill: 'stamina', playedOn: '2026-09-25', rate: '1.00', mods: 'NM' }); await page.goto('/projects/game-tracker/osu'); await expect( page.getByRole('progressbar', { name: 'All map goals', exact: true }) ).toHaveAttribute('aria-valuenow', '0'); await page.getByRole('button', { name: 'Achieved!', exact: true }).click(); await expect( page.getByRole('progressbar', { name: 'All map goals', exact: true }) ).toHaveAttribute('aria-valuenow', '100'); await expect( page.getByRole('progressbar', { name: 'Full combo goals', exact: true }) ).toHaveAttribute('aria-valuenow', '100'); }); test('BO2 armoury fits every gun, enforces caps and advances all rank progress with each level', async ({ page }) => { const userId = await register(page.request); const cod = '/projects/game-tracker/call-of-duty/black-ops-2'; const [m27] = await sql`select i.id from tracker_items i join tracker_games g on g.id=i.game_id where g.slug='black-ops-2' and i.name='M27'`; await page.setViewportSize({ width: 1280, height: 720 }); await page.goto(cod); await expect(page.locator('.weapon-card')).toHaveCount(40); for (const [width, height] of [ [1280, 720], [1920, 1080] ]) { await page.setViewportSize({ width, height }); const bounds = await page.locator('.weapon-card').evaluateAll((cards) => cards.map((c) => { const b = c.getBoundingClientRect(); const section = c.closest('section')!.getBoundingClientRect(); return { name: c.getAttribute('aria-label'), fits: b.bottom <= section.bottom + 1 && b.bottom <= innerHeight && b.right <= innerWidth && b.height >= 29 }; }) ); expect(bounds.filter((b) => !b.fits)).toEqual([]); expect(await page.evaluate(() => document.documentElement.scrollHeight)).toBeLessThanOrEqual( height ); await expect .poll(() => page .locator('.weapon-card img') .evaluateAll((images) => images.every( (i) => (i as HTMLImageElement).complete && (i as HTMLImageElement).naturalWidth > 0 ) ) ) .toBe(true); } for (const name of [ 'M27', 'AN-94', 'DSR 50', 'Peacekeeper', 'MP7', 'Vector K10', 'Skorpion EVO' ]) { await expect( page .getByRole('article', { name, exact: true }) .getByRole('button', { name: `View ${name} artwork` }) ).toBeVisible(); } const card = page.getByRole('article', { name: 'M27', exact: true }); const levels = page.getByRole('progressbar', { name: 'Weapon levels', exact: true }); const overall = page.getByRole('progressbar', { name: 'Game completion', exact: true }); await expect(levels).toHaveAttribute('aria-valuenow', '0'); await card.getByRole('button', { name: 'Level up M27', exact: true }).click(); await expect(card.getByRole('button', { name: 'Edit M27 level and prestige' })).toHaveText( 'LEVEL2/17' ); const first = Number(await levels.getAttribute('aria-valuenow')); expect(first).toBeGreaterThan(0); const before = Number(await overall.getAttribute('aria-valuenow')); await card.getByRole('button', { name: 'Level up M27', exact: true }).click(); await expect .poll(async () => Number(await levels.getAttribute('aria-valuenow'))) .toBeGreaterThan(first); await expect .poll(async () => Number(await overall.getAttribute('aria-valuenow'))) .toBeGreaterThan(before); expect( ( await action(page.request, cod + '?/saveRank', { itemId: String(m27.id), level: '18', prestige: '2' }) ).status ).toBe(400); expect( ( await action(page.request, cod + '?/saveRank', { itemId: String(m27.id), level: '17', prestige: '3' }) ).status ).toBe(400); expect( ( await action(page.request, cod + '?/adjustWeaponRank', { itemId: String(m27.id), field: 'prestige', delta: '1' }) ).status ).toBe(400); await action(page.request, cod + '?/saveRank', { itemId: String(m27.id), level: '17', prestige: '0' }); const simultaneous = await Promise.all( [1, 2].map(() => action(page.request, cod + '?/adjustWeaponRank', { itemId: String(m27.id), field: 'prestige', delta: '1' }) ) ); expect(simultaneous.filter((r) => r.status === 200)).toHaveLength(1); expect( ( await sql`select level,prestige from tracker_weapon_ranks where user_id=${userId} and item_id=${m27.id}` )[0] ).toMatchObject({ level: 1, prestige: 1 }); await page.reload(); await expect(card).toContainText('18/51'); await action(page.request, cod + '?/saveRank', { itemId: String(m27.id), level: '17', prestige: '2' }); await action(page.request, cod + '?/saveRank', { level: '42', prestige: '3' }); await page.reload(); await expect(card).toContainText('51/51'); await expect(card.getByRole('button', { name: 'Level up M27' })).toBeDisabled(); await expect(page.getByRole('img', { name: 'Prestige 3 emblem' })).toBeVisible(); const progress = await overall.getAttribute('aria-valuenow'); await page.goto('/projects/game-tracker'); await expect( page.getByRole('progressbar', { name: 'Black Ops II progress', exact: true }) ).toHaveAttribute('aria-valuenow', progress!); await page.goto(cod); await page.setViewportSize({ width: 1280, height: 720 }); await page.screenshot({ path: '/tmp/bo2-verified-desktop.png' }); await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: '/tmp/bo2-verified-mobile.png', fullPage: true }); await expect .poll(() => page.evaluate(() => document.documentElement.scrollWidth)) .toBeLessThanOrEqual(390); });